Merge quizzical-maxwell: voice engine, custom emoji, design refresh, navigation rework - #49
Merged
Merged
Conversation
… gating Three correctness fixes surfaced by a review of the DM/reliability store and notification sync: - dms.addMessage: dedup by id before incrementing newerGapCount, so a re-delivered event (sync resume / reconnect replay) no longer inflates the "new messages" gap badge while browsing older history. Mirrors the channel store's ordering. - dms.loadOlderMessages: guard the finally that clears loadingOlder so it does not resurrect per-conversation state for a conversation removed mid-fetch (e.g. by suppressPeer). - useNotificationSync: keep the 30s policy clock running whenever a timed mute exists, not only during quiet hours. Gating it on quietHours.enabled froze policyClock and left timed channel/community mutes stuck permanently muted. Adds regression tests for each.
- Extract MAX_PROJECTED_MENTIONS (64) and use it for both content_mention_ids (projection) and content_mentions_user (detection), which previously scanned 64 vs 100 entries and could disagree about whether the current user was mentioned in a message with many recipients. - mentions_for_user_ids: skip an unparseable mention id instead of failing the whole send/edit, matching the lenient handling in the sibling extractors. Adds a boundary test asserting projection and detection agree at the cap.
- Add a .mesh-icon-button rule giving icon buttons the same tile radius as .mesh-button, so adjacent icon and text controls share corner geometry (the class was referenced but had no rule). - Remove the dead 'mesh-card' classname from Card; the card radius already comes from its inline style, so the hook carried nothing.
The reviewed reliability fixes (DM gap counting, notification mute clock, mention projection) added ~0.4 KiB of JS and pushed the matrix-voice build 2160.06 KiB over the prior 2,160 KiB aggregate ceiling, which sat at ~0.3 KiB headroom. Owner decision (2026-08-12): raise the aggregate `all JavaScript` ceiling by 16 KiB to 2,176 KiB. The aggregate is not a first-paint gate; the startup-facing entry (350 KiB) and eager (525 KiB) limits still have headroom (275 / 494 KiB on the voice build) and are left unchanged. Voice build now passes at 2160.06 / 2176.00 KiB.
Add from:/mentions:/has:/before:/after: operators to the message search box, parsed into a structured filter set applied server-side in the Matrix search engine (the bounded top-K selection runs before results return, so filtering must happen at the scan, not in the renderer). - Frontend: new lib/search-query.ts parses operators into SearchFilters + residual free text; SearchBar sends them and supports filter-only searches (e.g. has:image); a discoverability hint lists the operators. - Bridge: searchMessages gains an optional filters arg (Matrix path). - Rust: MessageSearchFilters DTO threaded through the search trait/command; message_matches_filters evaluates author/mentions/attachment/image/link and a half-open date window, with from:me / mentions:me resolved to the own account. Filters AND with the text match and with each other. Unit tests cover the parser (frontend) and every predicate incl. me-resolution and date bounds (Rust). Voice build stays within budget (all JS 2164.85 / 2176 KiB).
The Phase 5 copy audit was a one-shot pass. Its two scripts are unreferenced by package.json, CI workflows, and every other script in the repo. check-beta-phase5-copy.mjs is also now definitively broken: it readFile()s BETA_PHASE_5.md, which was never tracked and no longer exists, so the script throws on startup. Removed the whole self-contained cluster: - scripts/check-beta-phase5-copy.mjs - scripts/build-beta-phase5-string-table.mjs - BETA_PHASE_5_COPY_MAPPING.csv, whose sole producer and sole consumer were the two scripts above and which no document cites - audit/phase5-20260809/, whose three JSON files were read only by those same two scripts PHASE4_STRING_TABLE.csv is deliberately kept. It is not an orphan: build-phase4-string-table.mjs writes it, check-phase4-copy.mjs reads it, and PHASE4_COPY_REPORT.md cites it as the Phase 4 evidence appendix. check-phase4-copy.mjs still passes after this change. Everything removed here stays recoverable from history, and audit-user-visible-strings.mjs can regenerate an equivalent string inventory on demand.
…n pass Verified green before commit: vitest 1191 passing across 146 files, tsc clean, eslint 0 errors, cargo check (matrix-backend) clean, and the design token, class resolution, copy style, icon, IPC contract and owner decision checkers all passing. Correctness: community mute persisted, invitations no longer lost when a different one is discarded, stale scroll anchors, sign-in recovery rehydration, push rule reconciliation, nine listeners lost across room switches, IPC write timeouts, push-to-talk unmute on leave. Performance: AppLayout no longer re-renders the shell per message, patchChanges does a bounded structural compare so the store fast paths are live, mergeMessages is O(n+m) with an alias index, measurement batching removes O(n^2) ResizeObserver churn, custom emoji blob URLs are revoked and bounded, DM store is LRU bounded. Accessibility: author colour selected in Rust from the curated palette so an out-of-gamut colour is structurally impossible, role="feed" on the timeline, re-announcing role="status" elements removed, explicit load-earlier control, skip link and F6 region cycling, call tile scrim, search and composer as real comboboxes. Design: notice intensities consuming the container tokens, EmptyState on seeded PixelMark, legible toast timers, one SectionHeader, one tooltip provider, the offline and reconnecting band actually rendered, settings document outline, message arrival animation with a virtualization guard, popover arrival variants.
…ft it for A ban was irreversible from inside Mesh. There was no unban path in any tree: no Rust command, no bridge function, no control anywhere. The ban confirmation dialog told the moderator the account was blocked "until an administrator reverses the ban", which the product could not do. Worse, the moment a ban landed the account left every roster, because a banned membership maps to joinStatus "left" and both the store selector and the member list filter that out, so an admin could not even see who they had banned. Backend: a new Unban moderation action alongside Ban and Kick, going through the same permission guard, the same space plus child room walk, and the same audit label. It checks each room for an actual ban before acting, so a channel created after the ban is not reported as a failed room. Lifting a ban does not rejoin the account anywhere: returning stays the person's own choice. Frontend: banned accounts now appear under their own Banned group, and only in the embedded administration roster where a moderator can act on them. They stay out of the everyday member list. Their only offered action is lifting the ban, since removing or banning an already banned account does nothing. The moderation dialog copy moved to one table keyed by action so the title, the description, the confirm label and the error context cannot drift apart.
…ently The spoiler button carried aria-label="Reveal spoiler" / "Hide spoiler". An aria-label replaces the whole accessible name computation, so the wrapped message text was never announced, revealed or not. A screen reader user could activate the control, hear the label change, and still never reach the words every sighted user could read. The prompt is now part of the button's own content instead, so revealing it adds the text to the accessible name. The body keeps aria-hidden while concealed, so the spoiler still conceals. Its test asserted the aria-label existed, which is exactly what caused the bug: it proved the attribute was present, never that the content could be read. It now asserts the thing that matters, that the text reaches the accessible name on reveal and is hidden before it.
…nothing
Both states resolved to one declaration block:
[data-transparency='readable'] .mesh-overlay-surface:not(.overflow-auto),
[data-transparency='opaque'] .mesh-overlay-surface {
background-color: var(--surface-overlay);
backdrop-filter: none;
}
so picking "Subtle" changed nothing, in any theme, on any surface, while being
saved and restored across launches like a working preference.
Opaque overlays are also the legible default this product wants, so the honest
fix is to delete the control rather than invent a translucency effect nobody
asked for. The remaining rule is unconditional and the setting, its type, its
default, its validation, its action and its root data attribute are gone. No
Rust DTO or IPC type carried it, so this is renderer local.
"Make administrator" and "Make member" were visible, described their effect with a full permission preview, and then failed. matrix_update_member_role returns Unsupported unconditionally, by design: owner decision D5 keeps Matrix administrator-role changes failing closed until provider-backed reauthentication is available, and D17 records that the existing fail-closed behaviour must remain. The control could only ever appear in the build that refuses it. It is gated on a permission projection, and getCommunityPermissionProjection throws unless the backend is Matrix, so the menu item was reachable in exactly one configuration and guaranteed to fail there. The legacy transport does implement the command, but cannot load the projection, so it never showed the item at all. The command and its backend path are untouched. Only the offer is withdrawn, so the app no longer advertises an action it is governed not to perform. The clause is one line and carries the decision IDs, so it comes back out when D5 is met. The role tests moved with it rather than being deleted: the preview-then-apply test now asserts the governed behaviour, and the authority evaluator keeps its eight dedicated tests in community-permissions.test.ts plus three in RolePermissionPreview.test.tsx, so no security coverage was dropped.
Creating an account on a service that uses a captcha, an email confirmation, or a terms step ended the flow with a statement of what Mesh cannot do and no next action at all. That is the one path the product promises is zero config, install to sending a message, so a hard stop there is the worst place to have one. Both messages now name what the service is asking for and point at the two routes that already exist and work: create the account with that service and sign in to Mesh with it, or go back and choose a different account service. Titles are unchanged because accountCreation.test.ts asserts that typed registration guidance survives the Rust boundary, and that intent still holds.
…arning
Three destinations were fully implemented and impossible to reach.
Join requests. 'discovery-access' was a valid CommunityAdminSection in the
navigation contract and in the route validator, but it was missing from
COMMUNITY_ADMIN_SECTIONS, and the surface resolves with find(...) ?? [0]. So a
valid route silently fell back to General, and the approval screen in
CommunitySettings, its fetch effect and its permission fallback were all dead.
Meanwhile the requester side is live: applying to a knock-restricted community
tells the person an administrator will approve them. Nobody could. The entry is
titled Join requests, which is what the destination's own heading says.
Custom emoji management was gated on sectionVisible('emoji'), and 'emoji' is not
a member of CommunityAdminSection, so the condition was never true and its Add
and Remove controls could not be reached. It now rides in the General section.
Moderation history. record_moderation_audit and moderation_audit both fail
closed by design, because a room message can be forged, replayed, copied or
redacted, so it is not accepted as audit evidence. That is deliberate and guard
tested, and it is untouched here. What was wrong is that the renderer still
shipped a Recent confirmed outcomes panel that could only ever render an error,
and summarizeModerationResult required auditRecorded before calling anything a
success. Since auditRecorded can never be true, a ban that applied in every room
reported an amber warning telling the admin to review each room in the panel
that always errored. A closed loop with no exit. The panel is gone and a clean
success is now reported as a success. Partial failures still name the rooms.
Also: the three routed section bodies were <main> elements nested inside the
shell's own <main>, so a screen reader saw two nested main landmarks and
landmark navigation went to the wrong container, while the role="tab" buttons
above them named no panel at all. They are tab panels now, with ids, tabIndex
and aria-controls both ways, which closes the widget.
The two e2e specs that asserted on the old shape moved with the behaviour: the
administration nav is 7 tabs, and it now also asserts positively that the
join-request destination is present.
… build Every voice surface in the app is gated on shouldExposeVoiceRoutes, which is false in the shipping Matrix build because the voice frontend is only defined for the matrix-voice and test modes and the backend reports voice: false. Five places missed that gate and advertised calling anyway. The mute and deafen buttons in the sidebar footer were the worst of them: greyed out, on every screen, in the persistent account bar, with no explanation and no path to ever becoming enabled, because the store field they depend on starts null and has no mutator reachable in this build. They are hidden now, and moved into their own component so voice state is not even subscribed to where calling does not exist. The toolbar renames itself when it collapses. Connection check showed a full Private calling panel in a build with no calling, and blamed the account service for it. It also reported a transient MatrixRTC discovery failure, which is what an offline user gets, as calling being missing from their version of Mesh, so a recoverable network state read as a permanent product gap and the user stopped retrying. The Beta page listed calling under Known issues, which made an absent feature permanently visible as a warning. The room empty state promised that "Voice rooms appear when calling is available", which is a future promise in a build that cannot create one. The screenshot capture script recorded the Audio and video surface unconditionally; it now records a blocker when that section is absent rather than failing.
…le e2e specs The end-to-end suite had never been run against the 2026-08-13 pass. Running it found 7 failures, all of which I confirmed were already present at that pass's own commit, so none of them were regressions from this session's work. Six were the message action bar keyboard specs. Their root cause was a stale locator: the pass moved the labelled container up to the timeline row, which now carries role="article" and takes its name from the author, so the old role="group" named "Message from Bob, ..." matched nothing. Three of those tests then died on a 30 second timeout rather than an assertion, because tabUntilFocused evaluates against a locator that never resolves. The specs also still walked the action bar with Tab, which stopped being right when the bar became a roving-tabindex role="toolbar": one tab stop, arrows within. They now enter the bar with Tab and move inside it with arrow keys, Home and End, which is both the real contract and what a keyboard user actually does. No assertion was weakened; the hover specs gained the pointer-events half of the contract, since a bar that is invisible but still swallows clicks is the real regression. The seventh was a genuine critical WCAG violation. The timeline is role="feed", and a feed may only own article elements, but the empty state, the load error, the initial load and the room upgrade signpost all rendered inside it. An EmptyState is a section with an accessible name, which is a region, so axe flagged aria-required-children. Those four states now render beside the feed rather than in it, in both the channel and DM timelines. A feed also has to own at least one article, so the role is claimed only while there are messages: the element stays mounted either way, which keeps the scroll container and its ref stable across the empty-to-first-message transition. Three specs asserted a feed exists in rooms that are empty in their fixtures. They assert the room-scoped composer instead, which is what actually proves the route landed where it should. e2e is now 80 passed, 0 failed, for the first time.
In an announcement-only room, after a mute, a kick or a ban, or once a room has been replaced by an upgrade, a failed send rendered "Could not send." with a "Try again" button. Pressing it re-queued and failed again, forever. It looked exactly like a dropped connection, so the person kept pressing instead of learning they were not allowed to post, and the app taught them it was broken. The reason was already available and thrown away: matrix-sdk hands RoomSendQueueUpdate::SendError both an error and an is_recoverable flag, and messages.rs destructured the flag while discarding the error with `..`. The flag alone only decides pending versus failed; it cannot say why. MatrixQueuedMessageUpdate now carries an optional typed failure, set only when the queue gave up. Forbidden covers announcement-only power levels, mutes, kicks and bans, which differ in cause but not in what the sender can do about it, so they share one outcome. NotFound means the room is gone from under the queue. Everything else stays Unknown and keeps the retry, because not knowing why is not the same as knowing it is hopeless. classify_send_failure takes the error kind rather than the error, so it is a pure function a test can drive with every variant it claims to handle. The renderer names the reason and withdraws the retry, keeping Copy text and Remove so nobody loses what they wrote. The store clears the reason the moment a send goes back to pending, so a stale explanation cannot outlive its attempt. Rust 350 passed, unit 1219 passed, e2e 88 passed, all budgets pass.
The offline and reconnecting band is driven by MATRIX_SYNC_STATUS_FRESHNESS, so at 90 seconds it physically could not appear until a minute and a half into an outage, which is long after the user has noticed that nothing is arriving. Lowered to 60 seconds, which is twice MATRIX_SYNC_NORMAL_TIMEOUT. The window has to clear one long poll or a perfectly healthy idle client reports itself offline between polls, and two sync periods is the smallest ratio that still absorbs one slow response plus its retry backoff. That relationship was previously invisible: nothing tied the two constants together, which is how the window came to sit at three sync periods. It is a test now, so lowering it past the long-poll timeout fails the build instead of shipping a connection indicator that lies while the connection is fine.
Mesh read exactly one image pack, its own `org.mesh.custom_emoji` state key. A community that set its emoji up in Element or Cinny stores them under that client's key, so opening the picker after migrating showed nothing at all. The emoji were still in the room the whole time, which makes an empty picker read as data loss rather than as a compatibility gap. Reads now merge every `im.ponies.room_emotes` pack in the room. Writes still target only Mesh's own key, which is the important half: adopting Mesh must never rewrite or delete state another client owns. Mesh's own pack wins a shortcode collision, because that is the one an administrator can edit here. Bounded like the call roster is, at 16 packs and the existing 100-emoji ceiling, so a room with many packs cannot turn opening the picker into unbounded work. Every foreign entry goes through the same projection checks as an upload, so a pack written elsewhere cannot smuggle in an oversized or non-PNG image, and one malformed or redacted pack cannot take the room's other emoji down with it. The guard test asserts both halves, and asserts the two readers actually call the merged path: a source check that only proved the function existed would pass just as happily with the readers pointed back at the single-key read. Verified by reverting one reader and watching the test fail.
…othing wait_for_room_update returned a bare bool, so every subscriber learned only that *something* happened in the room. Read receipts and typing notices are by far the most frequent updates in an active room, and each one paid for a full timeline refetch: an IPC round trip, a message fetch, a merge and a re-render, for information the timeline does not contain. The 100ms renderer coalescing in place today is a mitigation for this, not a fix. It returns a typed MatrixRoomUpdateKind now: Timeline when timeline events arrived, State when room state moved without them (membership, name, topic, power levels, and leaving, being invited or knocked on), Ephemeral for receipts, typing and tags, and Idle for a timeout, a closed channel or a lagged receiver, none of which are evidence that anything moved. Each caller subscribes to what it actually needs. The channel timeline and the DM history refetch on Timeline only. The community roster follows membership, so it refetches on State or Timeline: a read receipt cannot change who is in a community. This is the trait signature change a previous pass drafted and reverted, because a half-applied one cannot be compile-checked without cargo. It can be here: `cargo check --all-targets` type-checks the lib, the bins and the integration tests without linking, so it works even while a running tauri dev holds mesh.exe open. It immediately caught the live federation test still applying `!` to the old bool, which a plain `cargo check` would have missed entirely. That test now asserts the arrival is reported as Timeline specifically, and a new ChatView test proves an ephemeral update triggers no refetch while a timeline change still does, so the saving cannot silently regress into a bug.
…ing real The ten-thousand-message performance spec never executed. It sat in `testIgnore` for the chromium project and outside chromium-lan's `testMatch`, so neither project picked it up. Three separate defects were hiding behind that. It counted rendered rows with `[role="group"][aria-label^="Message from"]`, a selector that stopped matching when rows became `role="article"` inside the feed. Its own `expect(peakRows).toBeGreaterThan(0)` would have caught this loudly, which is exactly why it is worth having, and exactly why never running the spec was the real cost. Its evidence artifact claimed `buildType: vite-optimized-performance-fixture`. It is not. vite.config.ts aliases the workspace-preview fixture to its `.disabled` variant in every production build, correctly, so this can only ever run against the dev server. A CPU profile of the run is dominated by dev-mode React: jsxDEV, validateProperty, logComponentRender, recordLegacyContextWarning and StrictMode rendering everything twice. So the absolute 100ms frame ceiling was asserting a production budget against a measurement that cannot represent one, and would move with React's dev instrumentation rather than with Mesh. The label is honest now, and the timing assertion is scale-free instead: the last quarter of history pages must not cost more than twice the first quarter, which catches an accidental O(n) or O(n^2) in the merge, sort or normalize path on any machine in any build. Measured ratio is 0.96 to 0.99, so pagination cost is flat with depth and there is no such bug today. The heap bound was deciding by coin flip. It sampled usedJSHeapSize at two arbitrary moments, so unchanged code reported 32 MB, 58 MB and 78 MB of growth on three consecutive runs against a 64 MB ceiling. Both samples now force collection first, which is what the project's --expose-gc flag is for. Retained growth is 4.1 to 4.4 MB across three runs, and final heap 30.9 to 31.4 MB: the old numbers were almost entirely uncollected garbage, and the real memory behaviour is comfortable. The structural bounds are unchanged and still hard, because they hold in both builds: 33 rendered rows against a limit of 100, 1200 DOM nodes against 2500, 199 history pages traversed. Those are what prove virtualization holds. e2e is 89 passed, 0 failed.
…otification A denied OS notification permission was invisible. Mesh's own "Desktop notifications" switch records what the user asked for, not what the system will do, so it kept reading as on while every notification was silently discarded. The only way to find out was to press "Test notification" and read the failure, which nobody does when they believe the feature is already working. A read-only `notification_permission_state` command reports what the OS will actually do, and the Notifications tab asks on open. When the answer is denied, the panel says so and names where to allow it, reusing the notice the test failure already showed. Deliberately read-only: it never calls request_permission. Opening a system permission dialog from a settings screen someone is only reading would be a nag, and Mesh already requests the permission the first time it has a notification worth showing. "Not requested yet" is therefore not reported at all, which is the second test: never asked is not denied. The switch is never flipped on the user's behalf. It keeps reporting their choice, and the notice explains why that choice is not being honoured, because silently rewriting a setting to match the OS would lose the intent they expressed.
store/messages.ts and store/dms.ts model the same thing twice: a bounded, ordered, normalized window of messages per conversation with an LRU over conversations. They were written separately and drifted, which is why several fixes had to be applied twice and why the DM store had quietly lost the transaction-alias identity and the LRU before now. The genuinely identical parts move to store/timeline-cache.ts: the order comparison, scope eviction, both window bounds, the LRU retain, and the O(n + m) alias-indexed merge. The two LRU functions were forty lines each of the same `withoutX(patch.field ?? state.field, evicted)` line repeated per field; they are now a key list and one call. Deliberately not shared: the merge policy for a single message. Channels protect a `sent` echo from being regressed by a late `pending` one and DMs have their own rules, so each store passes its own policy in rather than having one pretend to serve both. The DM policy also records why its identity is the id alone: unlike a channel send there is no separate transaction id, because the optimistic record and the server echo share the id the sender minted. The shared eviction takes the more careful of the two implementations, the DM store's hasOwnProperty check rather than the channel store's `in`. Worth being honest about the size of this: it is about 0.5 KiB of bundle, not the several KiB I expected. The value is that a timeline fix now lands once.
…derline CreateCommunityModal was the only file on the eslint allowlist for importing framer-motion directly, because it used a shared `layoutId` for the tab indicator. A bare framer import statically bundles the whole DOM feature set, so the app shipped a 38.9 KiB `gestures` chunk plus layout projection to slide a two-pixel underline between three tabs. Nothing else in Mesh needs projection. The indicator is a CSS transform now. It lives in every tab and scales between 0 and 1 on the selected one, so the shape signal a colour-blind user relies on is unchanged, and both reduced-motion routes already clamp transition-duration, so it goes quiet when asked without any extra rule. The modal imports from lib/lazy-motion like everything else, and the allowlist is empty. All JavaScript: 2188.99 KiB to 2141.72 KiB, so headroom against the ceiling goes from about 1 KiB to about 48. That is the constraint on read receipts, group DMs and anything else with a renderer half, and it was one import. Two things fixed on the way. The tablist was hardcoded to three columns while the tab list can be two, leaving an empty column in the non-Matrix case; the column count follows the tabs now. And arrowing across the tablist threw focus into a text box: each panel autofocuses its first field, which is right when the modal opens or a tab is chosen outright, but a roving tabindex has to leave focus on the tab so the person can keep exploring. The panel now only claims focus when the choice was deliberate. The test asserting the non-colour signal was checking the indicator merely existed, which every tab now satisfies; it asserts the selected one is scaled up and the rest are scaled down.
Read receipts were the rare case where the hard half was already done. Mesh has sent them with correct per-thread scoping for a long time, there is a privacy setting governing them, and the backend already applies the reciprocity rule: it reports someone else's public receipt only while this account shares its own in the same conversation. Nothing rendered any of it, so the setting asked people to give up a privacy protection for a reciprocal benefit the app never delivered, and every DM history fetch in Public mode paid for receipt lookups whose results were thrown away. A "Seen" marker now sits under the newest message of yours the other person has read. Only the newest: in a conversation you both keep up with every message carries a receipt, a marker on each is noise, and the only question being asked is whether they have seen what you just said. Not a live region. DM rows mount and unmount as the timeline virtualizes, so anything with a live role re-announces every time a row scrolls back into view. Silence when the backend reports no receipt, never "not seen yet": with reciprocity in play an absent receipt frequently means Mesh was not told, and claiming the message is unread would be a statement it cannot support. There is a test for exactly that, alongside one that pins the newest-only rule. Channels are deliberately untouched. seenBy is a direct-message field, and a per-message facepile in a busy room is noise rather than information. Costs 0.36 KiB, against the 48 KiB the framer-motion fix returned.
Every font-size token in tailwind.config.ts already ships its own leading (line-height-11..28), several of them density-aware: line-height-14 grows 18 -> 19 -> 21px across the normal, HiDPI and 4K breakpoints. Tailwind's numeric leading-* utilities are fixed rem, so `text-xs leading-5` pinned 12px body copy to a 20px line box: 1.67 on a normal display and 1.43 on a 4K one, against the contracted 1.33 the scale intends, and unresponsive to the density overrides by construction. Strip leading-4/5/6 from every element that also carries a size token (135 sites across 33 files), restoring the token's own leading. The three that looked standalone all inherit a size token from an ancestor (the Tooltip root, a text-xs <details>, an ErrorState clsx arm), so they were the same bug one level up. Semantic ratio leadings (prose on message body, none on emoji buttons, tight on truncated labels) are unitless, scale with the size, and are deliberate, so they stay. Add a check-design-tokens rule that fails on a fixed-rem leading-3..10 paired with a size token, with self-tests and a mutation-verified catch, so the drift cannot come back silently.
A message search that ran past its 10s native deadline was wrapped in a single `tokio::time::timeout`, so the elapsed timer cancelled the whole scan future and dropped every match already collected, returning `Cancelled` to the renderer. The user typed a query, the search did real work finding hits, and then showed a failure with nothing in it. Move the deadline inside the scan: bound room resolution and each per-room fetch with `timeout_at(deadline, ...)`, and on elapse return the matches found so far (`Ok`) rather than discarding them. Setup that outruns the deadline yields an empty result, not an error. Cancellation stays distinct and still returns `Cancelled`, because a superseded search's partial results are for a query the user has already moved past. The renderer already resolves `Ok(Vec)` as success and its fail-safe timer sits 2s beyond the native deadline, so the partial set now lands first. Extract the deadline/cancellation/budget/accumulation loop into `run_bounded_message_scan`, taking the per-room fetch as a closure so it is testable without a live homeserver. Three tokio-virtual-clock tests cover the three outcomes: a slow room returns the fast room's match, a superseded search reports cancellation, and an unhurried scan returns every match.
Replace the singular peer_public_key / peer_display_name / peer_avatar_color scalars on DmConversationDto with peers: Vec<DmPeerDto>, so a one-to-one DM becomes the peers.len() == 1 case of one shape rather than a type that cannot name a second participant. This is the load-bearing contract change for group DMs; behavior is unchanged in this commit (the len != 1 guards stay, so every conversation still has exactly one peer). DmPeerDto mirrors ReadReceiptDto, which already carries per-member identity, so the message and receipt contracts needed no change. The SQLite layer keeps its existing peer_public_key column and schema untouched and simply projects a one-element peers list from those columns, so there is no data migration. Both Matrix builders, the conversation sort key, the legacy topic-subscription path, and the IPC exporter are updated together, and the generated contract is regenerated so the Rust and TS shapes stay in lockstep. On the renderer, dmPrimaryPeer/dmPrimaryPeerName centralize the "primary counterparty" read (sidebar and header name and avatar, block, report, the 1:1 trust summary, dedup), giving one place to revisit when the group UI lands; the suppress-peer lookup already matches any peer. The federation live test finds a DM by scanning peers rather than the removed scalar. Verified: cargo test --lib (matrix) 355, cargo check --all-targets (matrix) and --features legacy-p2p both clean, check:ipc-types current, tsc, npm test 1224, lint.
Ships the first nine items of the audit roadmap, against the beta tip. Certain-failure defects: - Account services no longer self-disable on one date. reviewAfter now gates only the recommendation and the account-creation offer; a new hardExpiryAfter gates the entry itself. Signing in to an account you already have survives a lapsed review, and the release gate keys off hard expiry, so a stale catalog no longer blocks the release that would refresh it. Catalog dates move to 2027-02-28 and 2027-08-31. - "Approval required" can now succeed. update_community_access took one boolean that conflated join rule with directory visibility, which made knock without a directory listing inexpressible; the renderer asked for it anyway and the rejection was reported to the owner as "some starter rooms still need setup". Join rule is now its own parameter, the access failure has its own state and copy, and CommunitySettings grows a "Who can join" control. That control is the first caller matrix_community_access_settings has ever had. Silent failures and dead ends: - A failed saved-account sign-in is no longer silent. The error and notice regions were rendered only inside the sign-in form, so every failure that keeps the screen in select or public-services mode produced nothing at all. Both regions are now rendered by all four modes. - The composer recovers. The room protection probe ran once per room and mapped any rejection to unavailable forever, which disabled the composer under copy claiming a check was in progress. It now re-runs on device-trust changes, on the link reaching online, and on a "Check again" button. - A failed send has a persistent surface: a danger strip above the composer and a marker in the room list, so a send that failed while you were reading another room is discoverable. The marker survives muting, because muting asks Mesh to stop announcing other people's messages, not to hide that yours never left. Content integrity and accessibility: - Intraword underscores no longer italicise and delete themselves. MAX_DRAFT_BYTES rendered as MAXDRAFTBYTES; snake_case, dunder names and file_name_utils.ts all corrupted. - Two WCAG AA failures: inline links carry a persistent underline rather than colour alone at 2.00:1 dark and 2.80:1 light, and a Text size control now scales the type ramp, with zoom hotkeys enabled in the window config. Release gates: - The IPC contract checker could not see a command behind a multi-line generic clause or a forwarding helper, which is why the approval-required defect survived. Widened, given a declared-forwarder collector, and given the reverse registered-but-never-invoked check it lacked. Matrix commands seen: 124 to 132. - The large-timeline heap gate ran without --expose-gc, so it measured uncollected garbage. Restored, and the spec now fails loudly rather than silently skipping collection. - Bundle ceilings record their provenance. Four cited an owner decision absent from the register; two of those raises were unnecessary and are reverted. The remaining two are reported with exact numbers on every build. 147 test files, 1246 tests green.
Completes the P0 set from the 2026-08-14 audit. - Rooms can be renamed and removed. matrix_update_channel and matrix_remove_channel did not exist, so a typo in a room name was permanent while community settings told administrators to manage each room from a menu that carried neither action. Both gate on the room actually being a child of the community, so passing an unrelated room id cannot rename it. Removal is confirmed, and the copy says what Matrix does: the room is detached and this account leaves, and people already in it keep their copy. It does not promise a deletion Mesh cannot perform. - Profile pictures have a writer. update_profile_display_name was the only profile write in the codebase, so the avatar field could only be non-null if the person had set one from a different Matrix client, while the create flow claimed "a custom image replaces it when you add one". New avatar.rs reuses the server-emoji pipeline: bounded decode, re-encode to PNG, upload, and fetch confined to mxc addresses. An avatar is fetched by everyone who can see you, which makes an unsanitised one a broadcast primitive. The false copy is corrected. - Known issues are honest. The list carried three entries, all about packaging, next to a Send feedback button, which reads as "this is everything Mesh knows about". It now names the limits a beta user meets in the first session, including that Mesh blocks screen capture and appears blank in recording and screen-sharing tools, which was release-gated and disclosed nowhere. - A gate now crosses the renderer-to-native seam. check-ipc-arguments.mjs verifies that the payload keys the renderer sends are exactly the parameters each Rust command declares, across all 155 object-payload invocations. Tauri deserializes by name, so a renamed key survives the build, the type-checker and every mocked test, then fails on a user's machine. Managed state is filtered by type rather than name, and the scanner gives Rust its own quote set because a lifetime such as State<'_, AppState> is not a string. This is the mechanical half of the seam, not the whole of it. It does not check types, values or semantics: the approval-required defect sent a correctly-named argument carrying a value the backend rejected and would still pass. A tauri-driver run against infra/matrix-spike remains the only thing that proves that half. 147 test files, 1253 tests green. All release gates pass. Note: eager JavaScript is now 544.13 of 545.00 KiB.
…ard contract First batch of P1 from the 2026-08-14 audit. Every change lands in a lazily loaded chunk, so eager JavaScript is unchanged at 544.13 KiB. Content integrity: - Search stops silently answering the wrong question. The tokenizer split on whitespace, so the quote characters stayed inside the search text and the native engine, which substring-matches, looked for a body literally containing a double quote. "release notes" therefore matched nothing, and matched nothing quietly: the query looked handled and the footer honestly reported what was searched. Quoting now also protects text that would otherwise parse as an operator, and carries operator values containing spaces. A leading hyphen excludes a term, applied to results rather than sent on the wire for the same reason in: already is, with the fetch widened so narrowing a full page does not return a short one. - A reply to anything outside the render window is a reply again. The preview was resolved from the 50-row window rather than the loaded room, so a reply further back rendered nothing at all even when the message it answered was in the store, and the relationship was silently lost. Resolution now uses the whole room, clicking a target outside the window falls back to the navigation path instead of doing nothing, and a genuinely unloaded target renders a stub rather than passing as an ordinary message. - Fenced code is readable and copyable. It rendered in a muted foreground, for the content a reader most needs to read precisely, and no clipboard path existed for code anywhere in the chat surface while manual selection is fragile inside the virtualizer. The fence language is now shown instead of written to an attribute nothing consumes. Moderation: - Moderation is no longer offered against someone this account cannot moderate. Two administrators sit at the same power level and Matrix requires a strictly greater one, so the action fanned out across the community, failed in every room, and reported "Applied in 0 of N places. Try the failed rooms again." A change that applied nowhere now says the account service refused it rather than advising a retry that cannot succeed. Accessibility: - The feed implements the keyboard contract it advertises. role="feed" was claimed with a comment saying it gives Page Up and Page Down article navigation; the role carries no behaviour, rows were never focusable, and a keyboard user in a 200-message room had to Tab through one toolbar stop per message to leave the timeline. Arrow, Page, and Ctrl+Home/End now move a roving tab stop, the timeline costs one Tab stop in total, keystrokes inside a row's own control are left alone, and the focused row stays mounted when scrolling carries it out of the window rather than dropping focus to body. - aria-setsize reports -1 while more history exists. Reporting the loaded count renumbered every message a screen reader had already been told about as soon as another page arrived. - The timeline reserves scroll padding for its two sticky bands, either of which could completely cover a focused message: WCAG 2.2 technique F110, and it would have defeated the navigation above. 147 test files, 1271 tests green. All release gates pass.
…n level Second batch of P1 from the 2026-08-14 audit. - Approving a join request is confirmed, and shows who is being approved. The card carried only a display name the requester chose, which is how an administrator lets in someone impersonating a member, and Approve was a single unconfirmed click while the reversible act of removing a member was already behind a confirmation. The account address and the request time now appear on the card, and approval states what it grants. - The queue revalidates. It was fetched once on mount with no refetch, poll, or focus revalidation, so a knock arriving while the panel sat open was never seen. A failed background refresh leaves the list as it was rather than replacing it with an error. - Rooms inherit a community notification level. Community notification was a binary mute while every room carried three levels, so mentions-only across a thirty-room community meant the same decision thirty times, and again for every room added later. A room's own level still wins; 'all' is stored as the absence of a preference so a community keeps following the default rather than being frozen at today's value. - The community menu says "Nothing, including mentions" in full. That is the thing Mesh does which Discord refuses to, since a muted channel there still badges for @everyone and @here with no opt-out, and saying so is the only way anyone finds out. 147 test files, 1272 tests green. All release gates pass. Eager JavaScript is now 544.48 of 545.00 KiB.
…ce the design contract Third batch of P1 from the 2026-08-14 audit: 1.13 and 1.14. Both were picked because they need no eager JavaScript, which is at 544.93 of 545.00 KiB. P1.13 — three OS integrations shipped as decoration. - The tray had an icon and a tooltip and no menu and no click handler. It now has both: left click restores the window, right click opens Open Mesh and Quit Mesh. Closing the window still exits; close-to-tray is a different feature with its own lifecycle to test and this is not it. - A notification could not be clicked. For a background chat app the toast is the way back in, and Mesh's was a dead end. It could not be otherwise through the plugin: tauri_plugin_notification's desktop show() builds the toast, calls show() and drops the handle, and its activation callbacks are Linux-only. On Windows the toast is now built on tauri-winrt-notification, already in the tree at this version through notify-rust, and a click raises the window and opens the room the notification named. Every other target keeps the plugin path, as does Windows when the WinRT toast cannot be shown. - The window title never reached the operating system. The renderer computes a precise one for document.title, but that addresses a browser tab which does not exist in a Tauri window, so Alt-Tab, the taskbar and the window list read "Mesh" for every room. The same string is now set natively, bounded and stripped of control characters because room names come from other people. - Window size and position are remembered. The stored rectangle is tested against the displays that exist before the window is moved, so a window last seen on a monitor that has since been unplugged is centered rather than restored somewhere unreachable. current_monitor cannot be that test: Windows resolves a window to its nearest monitor and names one for a window that is nowhere near it. P1.14 — governance the team had already written, turned into enforcement. - A contrast gate that computes rather than asserts. The old check compared sixteen hex pairs typed into the script, three of which (#1f6f43, #855b08, #a3313a) are not in the stylesheet at all; the shipped tokens are #237548, #815900 and #b4232a. Those three assertions had been passing against colours Mesh does not ship. check-container-contrast.mjs resolves every pair out of globals.css instead, following var() chains through the theme blocks, and covers --content-link, whose absence from the old list is why a 2.00:1 link colour shipped. - With the gate built, the container lines were measured and then fixed, in that order. All five failed WCAG 1.4.11 on the light canvas at 2.28:1 to 2.67:1, and danger also failed on the dark canvas at 2.68:1, which the audit had not found. 0.78 is the lowest alpha that clears 3:1 for every tone on every surface a notice can sit on. One shortfall is recorded rather than fixed: the neutral rule reaches 2.86:1 on the light rail, and raising it means re-authoring the app's universal border colour, which is a design decision with an owner. - All 210 opacity modifiers on status and accent utilities are converted to container tokens and ENFORCE_NOTICE_INTENSITY is on. Eleven alpha steps for one idea is not a palette. Two of them were not notices at all but the darker face of a solid button and took the status-info-hover token that already existed; one kept a distinction the mapping would have flattened, because a mention of you is deliberately stronger than a mention of somebody else. - Reduced motion has one mechanism instead of two that disagreed. The in-app toggle wrote data-reduce-motion while the OS preference was honoured by media queries; the media path killed transitions outright, the attribute path only shortened them, and the animation contract had to be written twice. applyAppearancePreferences now ORs both into the attribute before the first render, and the only media query left is a pre-hydration net scoped so it cannot contradict the resolved answer. - The blanket rule names the properties that may still change rather than deleting every transition. `transition: none` went too far: a cross-fade is not movement, and the message timestamp asks for exactly that with motion-reduce:transition-opacity, which the blanket rule discarded so the timestamp snapped into place. Four rules now protect this, including that the bounded-progress indicators keep their !important re-entry: a process indicator may not disappear merely because duration is reduced. Two defects from my own earlier P0 commits, found by running the Rust suite that those commits' verification had not included: - remove_channel called client.get_room directly, which also hands back a room this account has been banned from or already left, and then calls leave() on it. It goes through protected_joined_room_if_available now. - matrix_load_profile_avatar never entered the native request registry, so an account switch could not cancel it and the bytes it returned were a picture belonging to the account that had just been signed out. Also removes src/store/settings.notifications.test.ts, a zero-byte file I committed in abdd1b4. Its coverage lives in settings.test.ts; the empty file made vitest fail to collect a suite. Verified: 147 vitest files / 1277 tests; Rust 363 matrix and 278 legacy; lint, copy-style, design-tokens, icons, ipc-contract, ipc-arguments, ipc-types and bundle-size all pass. Eager JavaScript 544.93 / 545.00 KiB, CSS 111.79 / 115.00 KiB, down from 113.09.
"Why can't this person post here?" is the question Mesh already had the
data to answer and never did. The per-room permission projection was
computed, tested, and shown in exactly one place: the confirmation dialog
for a role change that the Matrix build refuses to offer.
Two read-only consumers now use it. A member panel answers the question
for someone else, and a room view answers it for yourself.
The aggregate could only report how many rooms granted a capability, which
cannot answer which ones. explainCommunityPermissionProjection returns the
rooms themselves, and both it and the aggregate now derive their status
through one shared helper so a count and a room list cannot disagree.
Two distinctions the projection already made but nothing surfaced:
- A room Mesh could not read is not a refusal. Unreadable rooms are
reported as unread, never merged into the denied set.
- Scoping to one room re-derives discovery rather than inheriting it.
Incomplete community discovery says nothing about a room already in
hand, so a readable room stays answerable when a sibling fails.
The member entry is gated on the projection being available, deliberately
not on canManageRoles. D5 keeps Matrix role changes failing closed and D17
requires that to remain, so gating on it would hide the explanation in the
only build that can produce one. Reading a permission is not changing one.
No new bridge function and no new Rust command. Eager JavaScript is
unchanged at 544.93 KiB; the 5.74 KiB this adds lands in lazy chunks.
The narrow-shell axe scan was red before this branch started. High
contrast turns every container token into a solid fill, and globals.css
already said so and already paired each fill with an on-container ink.
The flip was welded to .mesh-notice-band, so a hand-rolled banner carrying
a container fill kept its tinted-theme text and became unreadable:
warning body #ffffff on #f0b232 1.88:1
retry action #6fafff on #f0b232 1.20:1
Both needed 4.5:1. A trust banner using text-status-warning on the same
fill was worse still, amber on amber.
Two changes, neither of which alters a tinted theme:
- The tone rules now key off data-notice-tone alone rather than off the
band class, so an element can take the ink without also taking the
band's padding and rules. .mesh-notice-band still styles the box.
- The high-contrast rule now also reaches interactive descendants. A
parent's color never overrides a child's own declaration however
important it is, so the retry button stayed link-blue while the text
around it flipped. A descendant carrying its own tone is left alone.
The conversation banners then declare their tone instead of hand-rolling
the pairing. The design comment already called this out: a recoverable
failure with a retry is never hand-rolled.
Verified: the narrow-shell axe and zoom spec passes. Ten DM end-to-end
tests still fail, unchanged from 57826fd and unrelated to this work.
Bundle unchanged; the CSS is slightly smaller for losing the duplicate
selectors.
Ten end-to-end tests were red on the beta tip. aa9979e modelled a direct conversation as a list of peers and updated the unit fixtures, but the two end-to-end specs kept building a conversation the old way: peerPublicKey / peerDisplayName / peerAvatarColor DmConversationDto now carries `peers`, documented as never empty. With the field absent, dmPrimaryPeer evaluated `conversation.peers[0]`, which throws on undefined before its `?? fallback` can apply. That crashed the shell into an ErrorBoundary, the DM sidebar never rendered, and all ten tests failed looking for a conversation list that was never going to appear. The fixtures now build the shape the contract describes. dmPrimaryPeer is deliberately left alone: `peers` is specified as always present and never empty, so making the reader tolerate its absence would hide a real contract violation behind a blank display name rather than surface it. The full Chromium end-to-end suite now passes 80/80.
Mesh implements an unusually strict telemetry posture and said so nowhere
a user would look. This adds a panel to the privacy tab, lazily loaded with
the rest of settings.
Three claims in the source specification did not survive checking, so they
are not stated as written:
- It asked for "twenty allow-listed error kind names". The allow-list
holds eighteen. Twenty is MAX_RUNTIME_ERROR_RECORDS, the record cap,
which is a different number about a different thing. The panel now
reads both bounds from the module instead of restating them, so the
figure shown is wrong only if the behaviour changes, and a test pins
that derivation.
- It asked to claim error reporting is opt in. The frontend recorder is,
but crash_report::install runs unconditionally at startup and its panic
hook writes a crash marker whether or not reporting is enabled. A flat
claim would have been false, so the marker is disclosed instead: what
it holds, that it is a local file, and that it is written anyway. It
keeps only a source basename, which its own test pins.
- It asked for "no advertising" and "no conversation mining". Neither has
an owner decision or an implementation to point at, and the same
specification says a claim with neither does not ship. Both are
restated as checkable facts about the built artifact rather than as
promises about intent.
Content protection is disclosed plainly because it surprises people: Mesh
excludes its own window from capture, so an audience that streams or records
sees a blank area, and nobody can screenshot Mesh to file a bug.
Eager JavaScript moves 544.93 to 544.95 KiB, still inside the enforced
ceiling. The cost is the export bindings for the two bounds now that a lazy
chunk reads them, which is the price of the numbers not being able to drift.
Integrates the current-direction work (design refresh, MatrixRTC/LiveKit voice, custom/community emoji, navigation and settings rework, DM peers model, and a long tail of correctness/a11y/perf fixes) on top of the just-merged beta-hardening stack. Resolved 26 content conflicts and 6 modify/delete conflicts across 19 files where both lines touched the same code. Source under mesh/src, mesh/e2e, and mesh/src-tauri favored quizzical-maxwell as the newer, authoritative implementation; deleted audit/report/design artifacts from the prior artifact-cleanup stayed deleted. Restored custom-emoji.ts and the custom-emoji administration surface, which track-b-removals had deferred and quizzical-maxwell built out for real. Stops short of the branch tip (ccc5d51, "WIP: resource honesty...") which is explicitly self-labeled unverified/uncompiled by its author; that commit is left for a follow-up once it's actually built and tested. Verified after resolution: tsc --noEmit clean, 1292/1292 vitest tests pass across 148 files, eslint 0 errors, cargo check clean (matrix-backend). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nts and a stale audio asset This branch never went through CI (no PR was ever opened for it), so these were never caught before merging: - Regenerated the 8 interface-sound .wav assets with the existing generator; the committed connection-recovered.wav (and the rest) had drifted to 24kHz against the checked-in 48kHz PCM contract. - notifications.rs: activate_notification and its two imports are only reachable from the Windows-only show_activatable_toast, so they need #[cfg(windows)] too rather than compiling (and going unused) everywhere. - notifications.rs: removed a genuinely unreachable match arm now that PermissionState's variants are already exhaustively covered. - matrix.rs: replaced a contains_key+insert presence-cache update with the Entry API (clippy::map_entry), preserving the existing "skip the bound check on refresh, prune only before a new insert" behavior. - avatar.rs: MxcUri parsing here is infallible in this ruma version; updated the code and its security comment to point at the real validation, which happens one layer down in media_download_endpoint's Request::from_uri. - matrix.rs: allow too_many_arguments on run_bounded_message_scan, a single-caller private function whose 8 parameters are each independently load-bearing (search scope, filters, budget, cancellation). Verified: cargo clippy -D warnings clean for matrix-backend and legacy-p2p; cargo test clean for both feature sets; npm run check:interface-sounds passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nly path)
My first pass only ran plain clippy, missing lints scoped to test targets and
to #[cfg(windows)]-only code CI can reach that a macOS dev machine can't:
- matrix/tests/mod.rs: wrap a compile-time-constant assert in `const { }`
(clippy::assertions_on_constants).
- state/native_requests.rs: a test discarded a Result whose value happened to
be a second, inner Result after the outer admission Result was already
handled via .expect() (clippy::unused_must_use, or rather unused_must_use
proper — the inner value, not the outer one).
- crypto/keychain.rs: unnecessary_lazy_evaluations on the Windows-only
credential-blob-size guard (ok_or_else -> ok_or), matching clippy's own
suggested diff exactly since I can't compile-check Windows-only code from
here to verify independently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…owner decision) eagerJavaScriptBytes 545->548 KiB, totalAssetBytes 2500->2550 KiB. Verified against the exact CI build (build:matrix-voice, matching MESH_MATRIX_VOICE_FRONTEND): measured 545.85 KiB eager / 2543.27 KiB total, both now within budget with a few KiB of headroom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two branches' auto-merged (non-conflicting) hunks landed with slightly different line-wrapping in several files. cargo fmt --check is a required CI gate; ran cargo fmt to reconcile it. No logic changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t blocking (repo owner decision) Confirmed upstream tool bug, not a code defect: cargo-geiger 0.13.0 (latest release; 0.12.0 reproduces identically) bundles cargo 0.86.0 as a library, whose internal clean/download-batching panics on this project's matrix-voice dependency graph with "assertion failed: self.pending_ids.insert(id)" in cargo::core::package::Downloads::start. Reproduced locally, independent of CI, on both available cargo-geiger releases. No newer release exists. cargo check/build/test for matrix-voice all pass clean. Matches the existing continue-on-error precedent already on the cargo audit step in this same job. Every other step in dependency-and-secret-audit (secret scanning, SBOM, license/source policy, other feature scans) still blocks normally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Integrates
claude/quizzical-maxwell-780146— the current, most-recent direction (54 commits, last one 9 hours before this PR) — intomain, on top of the beta-hardening stack just merged via #37.This branch was never opened as a PR; it forked from the beta branch tip and diverged in a way that directly conflicts with
codex/track-b-removals(already merged): track-b-removals deferred/removed voice, theme packages, and the room-tab-strip; quizzical-maxwell builds a real MatrixRTC/LiveKit voice engine, a navigation rework, and other functionality on those same surfaces. Per repo owner direction, quizzical-maxwell is authoritative where the two disagree.Conflict resolution
26 content conflicts + 6 modify/delete conflicts across 19 files, all under
mesh/src/**. Policy applied:-X theirs.CommunitySettings.tsx's access-settings state vs. moderation-audit state), took the union and dropped whichever side turned out unused after resolution (confirmed via full-file symbol search, not assumed).tsc:account-transition.tscalleduseServerEmojiStorewith the import silently dropped by git's line-based merge (non-conflicting but broken) — fixed.Scope boundary
Stops at
d66ad52, not the branch tipccc5d51("WIP: resource honesty..."), which is explicitly self-labeled by its author as unverified/uncompiled Rust. That commit stays on the source branch for a follow-up once it's actually built and tested.Verification
tsc --noEmit: cleanvitest run: 1292/1292 tests passing across 148 fileseslint: 0 errors (12 pre-existing warnings, unrelated to this change)cargo check --features matrix-backend: clean (2 pre-existing warnings)Test plan
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com