# Lotus Chat — Work Backlog **Repo:** `lotus` branch at `https://code.lotusguild.org/LotusGuild/cinny` **Deploy:** push to `lotus` → CI → auto-deploy to `chat.lotusguild.org` (~11 min) --- ## ⚠️ TDS DESIGN LAW — READ BEFORE TOUCHING ANY UI > **ALL Lotus Terminal Design System (TDS) styling — colors, animations, glows, borders, fonts, spacing — MUST come exclusively from `/root/code/web_template/base.css` CSS variables.** > Do NOT hardcode hex values. Do NOT invent new variable names. Do NOT deviate from the design tokens defined in that file. > The canonical variable reference: `--lt-accent-orange`, `--lt-accent-cyan`, `--lt-accent-green`, `--lt-glow-orange`, `--lt-box-glow-*`, `--lt-border-color`, etc. > Reference implementation for code patterns: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css) > This rule applies to EVERY task in this file without exception. --- ## 🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY > **Every feature we implement must feel native to the upstream Cinny app — indistinguishable from something the Cinny team would have shipped.** Reference: . > > Concretely this means: > > - **Use the `folds` design system, not bespoke UI.** Build with folds primitives (`Button`, `Chip`, `IconButton`, `Menu`, `MenuItem`, `Dialog`, `Modal`, `Input`, `Switch`, `Badge`, `SettingTile`, `SequenceCard`, etc.) and folds tokens (`color.*`, `config.space.*`, `config.radii.*`, `config.borderWidth.*`). No hardcoded hex/`rgba()` for UI chrome, no invented/undefined CSS variables. > - **Match Cinny's existing patterns.** Before adding UI, find the closest existing Cinny component/flow and mirror it (e.g. a new dropdown uses `Button`+`PopOut`+`Menu`+`MenuItem` like the rest; a new modal has a `Header` with a close `IconButton`; a new setting is a `SettingTile` inside a `SequenceCard`). Consistency with stock Cinny beats personal style. > - **Lotus-custom additions should be unobtrusive** and fit Cinny's visual language, spacing, and interaction conventions — a stranger using Cinny should not be able to tell which features are ours. > > **The ONE exception:** explicit **Lotus Terminal Design System (TDS)** features, which intentionally have their own distinct look and follow the **TDS Design Law** above. TDS styling is opt-in (only active in Lotus Terminal mode); everything else must look and feel like native Cinny. --- Completed features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md). --- ## 🔎 Audit findings — Wave 1 (2026-07) Bug-hunt of the Tier-1 high-risk areas (notifications/unread/receipts, threads, calls host-side, Element Call fork) by 4 parallel deep-audit agents. `[T#]`=threads, `[N#]`=notifications, `[C#]`=calls host, `[EC#]`=fork. **✅ FIXED (2026-07):** all 🔴 (T1, N1, N2); web 🟠 (T2, T4, N3, N4); calls-host 🟠 (C-H1, C-H2, C-H3) + 🟡 (C-M3, C-M4, C-M5, C-M6, C-L4, C-L6) — reviewed (the C-H2 AFK rewrite + C-H1 rejoin guard verified). EC-fork 🟡 (EC1–EC6) fixed on `element-call:lotus` (**needs a republish**). Web + calls gate-green (677 tests + `threadReceipt.test.ts` locking the T1 regression). **Still open (low tail):** C-M1/C-M2 (DOM-hack fragility — retire via the fork), C-L1/L2/L3/L5/L7/L8, and N5, N6, T5, T6, T7. ### 🔴 High — data-integrity / broken core UX | ID | Defect | File:line | Repro | Fix sketch | | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | | **T1** | **`markThreadAsRead` corrupts the room's MAIN read marker** — sends `sendReadReceipt(latestEvent, Read)` with `unthreaded` omitted (→false); `latestEvent` is the thread **root** when replies aren't loaded, so the SDK writes a `thread_id:"main"` receipt at the old root → `getEventReadUpTo` jumps back → room re-lights. **Same class as the P6 regression, second path.** CONFIRMED. | `features/room/thread/useThread.ts:172` (fired on mount by `ThreadPanel.tsx:145-167`) | Open a thread whose root is an older message (or any thread before replies load) → previously-read main messages resurface as unread; room dot won't clear. **Likely a live cause of the current "unread won't clear."** | Mirror `notifications.ts`: use `thread.lastReply()` and bail if null, or `if (latestEvent.getId() === thread.id) return;` — only receipt a real reply. | | **N1** | Favicon + tab-title unread count **double-counts space aggregates** — `putUnreadInfo` writes an entry for the leaf room AND every ancestor space; `FaviconUpdater` sums **all** map entries. CONFIRMED. | `pages/client/ClientNonUIFeatures.tsx:97-103` (FaviconUpdater) vs `state/room/roomToUnread.ts:67-74` | A room w/ 3 highlights nested in Space→Subspace → tab title reads `(9)` not `(3)`. Orphan-only users unaffected (why it hid). | Sum only leaf entries (skip aggregates: leaves have `from:null`), or compute from `mx.getRooms()`. | | **N2** | **Missed notifications/sounds** — the `deliverNotification` dedupe cache (`unreadCacheRef`) is written only on incoming messages, never cleared on read. `unreadInfo.total` resets to 0 on read but the cache keeps the stale value. CONFIRMED. | `pages/client/ClientNonUIFeatures.tsx:364-408` | DM cadence "msg→read→msg→read…": after the first, every later message's notification+sound is suppressed (`unreadEqual(1,1)`). | Clear/refresh `unreadCacheRef` from a `RoomEvent.Receipt` listener, or dedupe on last-notified `eventId` (as the thread path already does). | ### 🟠 Medium — visible wrong behavior | ID | Defect | File:line | Notes | | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | | **T4** | Explicit per-thread **"All" override is defeated** by the room-count gate: `deliverNotification` returns early on `unreadInfo.total===0` and dedupes on `unreadEqual`, but a non-mention thread reply in a Mentions-only room doesn't bump the count → "notify all replies" silently drops. CONFIRMED. | `ClientNonUIFeatures.tsx:370-380` vs `:489-502` | Don't gate the thread path on room `total`; dedupe on thread event id (`lastNotifiedThreadRef`) only. | | **N3/T3+** | **Muted-thread-only room gets a phantom unread dot** — `getUnreadInfos` pushes a `{0,0}` entry (roomHaveNotification true from the muted thread's server count), which still lights the nav row + pollutes "unread only" filters. CONFIRMED. | `utils/room.ts:261-278` | Don't push when `getUnreadInfo`→`{0,0}` and `roomHaveUnread` is false. | | **N4/T3** | Reading a thread **DELETEs the whole room badge** — `handleReceipt` unconditionally `DELETE`s on any of my receipts; if the thread was already read (no `UnreadNotifications` PUT follows) the room's legit main-timeline badge vanishes until the next event. Also a PUT/DELETE ordering race. PLAUSIBLE. | `state/room/roomToUnread.ts:243-263` | For threaded receipts, recompute via `getUnreadInfo` PUT instead of blanket DELETE. | | **T2** | Thread **"Jump to Latest" doesn't reset the virtual window** — only bumps scroll count; when scrolled up with live replies arriving, newest replies aren't rendered so the chip lands on a mid/old event. CONFIRMED. | `features/room/thread/ThreadTimeline.tsx:462-468` | Re-anchor with `setTimeline(getInitialThreadTimeline(...))` like the main timeline's `handleJumpToLatest`. | | **C-H1** | `forceState()` **re-runs on every `JoinCall`** (no once-guard) → on an EC reconnect it resets live mic/video/deafen back to the join-time snapshot, clobbering the user's mid-call toggles. PLAUSIBLE (structurally confirmed). | `plugins/call/CallEmbed.ts:362-364,502-509` + `CallControl.ts:141-156` | Guard `onCallJoined` to `forceState` only on first join, or re-apply _current_ state on rejoin. | | **C-H2** | AFK auto-mute monitors the **browser default mic**, not EC's selected device → measures silence while the user talks on a non-default mic → **auto-mutes an active speaker**; also a 2nd OS mic indicator. PLAUSIBLE. | `hooks/useAfkAutoMute.ts:39-40` | Acquire with EC's selected `deviceId`, or tap EC's existing track. | | **C-H3** | Screenshare/spotlight **state observer goes stale** — `bodyMutationObserver` uses `subtree:false`, so when EC re-renders its controls subtree `observeControls()` never re-runs and the reflected screenshare/spotlight boolean freezes. PLAUSIBLE. | `plugins/call/CallControl.ts:168-200,274-287` | Observe the stable controls container with `subtree:true`, or re-query on each mutation. | ### 🟡 Low / minor / cosmetic / perf - **Calls host:** C-M1 deafen DOM-fallback leaks late-added `