@micha.bigler/ui-core-micha 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/WORK_ORDERS.md
CHANGED
|
@@ -28,8 +28,9 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
28
28
|
| NOTIF-6 | NotificationsProvider + bell on canonical dcm feed/* API | Single-WS-owner `NotificationsProvider`/`useNotifications` (initial REST seed via `feedApi.js` + one `/ws/notifications/` socket, reconnect w/ backoff) + generic MUI `NotificationBell`; additive exports only | 2026-07-27 | done | 6c63fb9 | Codex-built direct (no nesting), independent `reviewer`: single-WS-owner invariant traced clean (no way to break it — `authenticated`/`replaceNotifications`/`refresh` all referentially stable across unrelated re-renders, re-render test is genuine not a no-op). R1 (P2, real bug): the `notification.status` WS handler patched item flags but never adjusted `unreadCount`, so the badge went stale after a cross-tab/cross-device mark — the test had locked in the stale count as expected behavior. Fixed: delta computed from before/after unread state on the WS patch path, mirroring `mark()`'s own logic; test corrected + a new cross-tab regression test added. 57 passing, build clean. No new npm dependency. Full history + backend contract: dcm's `WORK_ORDERS.md`/`docs/design/notifications-platform.md`. Publishes ucm 2.11.0 |
|
|
29
29
|
| NOTIF-13 | Layer-1 transport extraction (pluggable realtime) | Extract the socket out of `NotificationsProvider` into a generic **subscribe-by-envelope** realtime primitive (shipped API: `useRealtime() -> subscribe(envelope, handler)`; the WO said "by message-type", but routing is on the `envelope` discriminator, deliberately NOT on the overloaded `type` field — that overload was the bug) so a second domain (messaging) can ride the same socket; the catch-all that turns every non-`notification.status` message into a feed entry is replaced by explicit dispatch with unknown types ignored. Additive: `NotificationsProvider` stays the sole mount point, its context value unchanged. Full spec: [work-orders/NOTIF-13.md](work-orders/NOTIF-13.md). | 2026-07-29 | done | ucm: 7a83ee9; dcm: de77335 | Tier 2 (cockpit consumes `NotificationsProvider` at 2.12.0 in prod). Phase A, the hinge: unblocks NOTIF-15 (jg bell adoption) and is the substrate for Phase B messaging. Canonical register row: django-core-micha `WORK_ORDERS.md`. Operator decision 2026-07-29: additive only, NO standalone `RealtimeProvider` apps must mount. Scope E (envelope contract + helper + doc) also touches dcm; note that the roadmap wording "dcm multi-stream consumer" overstates it — `push_to_users`/`NotificationConsumer` are already stream-agnostic. Codex failed twice on this WO with a genuine CLI/model-version error (`gpt-5.6-terra requires a newer version of Codex`), not a transient issue — orchestrator fell back to direct Claude implementation per the Codex→Claude fallback rule. Independent `reviewer` (sec_reviewer not required per WO): found R1 (P2, real bug — decoupling the REST seed from the socket connect opened a race where a live push arriving before the initial feed loaded would be silently discarded by `refresh()`'s unconditional `replaceNotifications()`; fixed via a `seeded` state gate restoring the exact original seed-then-connect ordering, re-verified 70/70 green), R2 (P3 — the WO's "must be called out in the CHANGELOG" has no home since ucm has no CHANGELOG.md convention at all; operator decision: document via commit message + this register entry only, no new file), R3 (register housekeeping, closed by this update). ucm full suite 70/70 + `pnpm build` clean; dcm notifications suite 92/92, full dcm suite 392/392. Published: ucm 2.13.0 (npm), dcm 2.33.0 (PyPI, verified live before this record) — no app pin bumps in this WO per spec. |
|
|
30
30
|
| NOTIF-12 | Popup channel via the wizard renderer | Make `popup` a real delivery channel: extract the presentational dialog shell out of `OnboardingWizard`, render popup-eligible notifications through it, and record seen/dismissed on `NotificationRecipient` (design D-F7: renderer reuse only, NOT the onboarding progress store); dcm `PopupDispatcher` stops being a logging stub. Full spec: [work-orders/NOTIF-12.md](work-orders/NOTIF-12.md). | 2026-07-29 | done | ucm: c8e222f, d7bdcbe; dcm: django-core-micha 1612429, 82fd89f | Tier 2 (extracts a renderer cockpit+jg run in production; the roadmap label "uncritical" is about urgency, not blast radius). **HARD GATE: starts only after NOTIF-13 landed + published** — consumes its envelope contract and `subscribe()`, and edits the same ucm files (AGENTS.md overlap rule). **SHIPS WITH ZERO PRODUCERS, deliberately** (operator decision 2026-07-29, made with the evidence): no notification type declares `popup` in `eligible_channels` anywhere in dcm/jg/cockpit/hram/spesix/survey_app, so the channel is wired but inactive until an app opts a type in. WO therefore requires (a) CHANGELOG wording that does not overclaim (NOTIF-4 R1 precedent) and (b) a test-local producer type proving the path end-to-end, so it is not untested dead code. Was NOTIF-11 (renumbered 2026-07-29 for the jg cutover/drop split). **Gate satisfied 2026-07-30** (ucm 2.13.0 / dcm 2.33.0); WO re-verified against the landed NOTIF-13 code and corrected in `345631d`: NOTIF-13 shipped the envelope as a **domain**-level discriminator, so a popup payload built the same way as a chip payload would be indistinguishable from it - the WO now mandates a `channel` field inside the envelope (never a second envelope value) plus feed de-duplication by `notification_id`, without which chip+popup double-counts the unread badge. **Codex failed twice with the same genuine CLI/model-version error as NOTIF-13** (`gpt-5.6-terra requires a newer version of Codex`) — orchestrator implemented directly in Claude per the Codex→Claude fallback rule, both scopes. Independent `reviewer` (background agent, `fork_context=false`, full diff of both repos): found R1 (P1, real bug — the WS payload only carried `notification_id`, but `feed/mark/` resolves ids against `NotificationRecipient.pk`; every `markSeen`/`markDismissed` from a session-live-only popup would either no-op or corrupt an unrelated recipient row once a producer opts in — fixed by adding `recipient_id` to both `ChipDispatcher` and `PopupDispatcher` payloads, dcm-side, with regression tests), R2 (P2 — the reused dialog shell always showed the onboarding "Setup"/step-counter header even for a popup, a real mislabeling once any producer exists — fixed via an optional `title` override on `WizardDialogShell`, defaulting to the prior exact text so `OnboardingWizard` stays bit-for-bit unchanged; popup now shows its own notification title), R3 (P2 — coexistence only gated on a *blocking* onboarding step, so a non-blocking step (e.g. `browser_push`) and a popup could stack two simultaneous MUI dialogs — fixed by broadening the gate to any active onboarding step, with a new regression test). All three fixed and re-verified: ucm 88/88 vitest + `pnpm`/tsc build clean, dcm 95/95 pytest (notifications package). Published (once pushed): ucm 2.14.0 (npm, CI-gated on push to `main`), dcm 2.34.0 (PyPI, same). No app pin bumps in this WO per spec. Own orchestrator security/exposure pass in parallel with the reviewer: no findings (content rendering follows the existing `NotificationBell` i18n-interpolation pattern, no new WS consumer, `prefs.py`/`router.py`/`resolve_channels` untouched). |
|
|
31
|
-
| DX-1 | Minimal Vite dev harness for ucm components | Give ucm a way to **render** its components during development: a `dev` script starting a Vite dev page that mounts surfaces with the repo's MUI theme, i18n and router context, driven by fixtures through an injectable mock API/realtime adapter, with light/dark and viewport switching. Harness code stays out of the published package (not exported, excluded from the build, zero runtime deps). | 2026-07-31 | done | 7ea7b26 | New `DX-*` prefix (ucm-local). Envelope: [work-orders/DX-1.md](work-orders/DX-1.md). Tier
|
|
31
|
+
| DX-1 | Minimal Vite dev harness for ucm components | Give ucm a way to **render** its components during development: a `dev` script starting a Vite dev page that mounts surfaces with the repo's MUI theme, i18n and router context, driven by fixtures through an injectable mock API/realtime adapter, with light/dark and viewport switching. Harness code stays out of the published package (not exported, excluded from the build, zero runtime deps). | 2026-07-31 | done | 7ea7b26 | New `DX-*` prefix (ucm-local). Envelope: [work-orders/DX-1.md](work-orders/DX-1.md). **Tier 2** (reclassified 2026-08-01 — it adds dependencies in a shared-core repo, two binding Tier-2 surfaces per AGENTS.md Tiering; a named independent `reviewer` did run, so only the label was wrong). **Adds devDependencies → approval-gated**; operator approval granted 2026-07-31 as part of the MSG-3 scoping decision. Gap found while scoping MSG-3: `package.json` had only `build` (tsc) and `test` (vitest) — no Storybook, no demo, no dev page, so every ucm surface ever shipped (auth, onboarding, notifications, charts) was verified only in jsdom or after a consuming app pinned it. Storybook deliberately not chosen (config surface vs. a repo with no story-writing habit). **Runs before MSG-3**, which is ~3700 LOC of UI with no consumer until MSG-5 and an operator-granted redesign licence that is only iterable if the result can be looked at. No version bump, no publish (dev-only). **Implemented (Codex, one pass):** `pnpm dev` (`vite`) boots a harness (`dev/main.jsx`) with MUI theme/i18n/router, light/dark + mobile/tablet/desktop switching, entries for a notifications surface (`NotificationBell`) and a standalone chart (`BarChart`, proving single-component mounting), plus a dedicated realtime-adapter demo entry. Mock transport (`dev/mockTransport.jsx`) overrides `apiClient`'s axios adapter for REST and provides a `RealtimeContext.Provider` for context-consuming components. Independent `reviewer`: one P2 — `NotificationsProvider` opens its own WebSocket internally rather than reading one from context, so the notifications entry opened a real, always-failing socket; fixed by the orchestrator with a `MockWebSocket` class registered as `window.WebSocket` while the harness is mounted, covering both consumption paths. Verified in the browser (both entries, dispatch button, no console errors/WS connection attempts). Build clean, `dist/` unaffected, `dependencies`/`peerDependencies` untouched (only `devDependencies` + `scripts.dev`), 104/104 tests green. |
|
|
32
32
|
| MSG-3 | ucm messaging surfaces (full parity) | `MessagingProvider` + API adapter + normalized cache, `ConversationList`/`Thread`/`Composer`/`ReadTicks`/`ReactionBar`/`PollCard`/`AttachmentList` + config/preferences and launchers, on the Layer-1 transport (`subscribe('messaging', …)`, `event_id` dedup, reconnect refetch); opaque-cursor pagination with reverse infinite scroll and optimistic send from day one; de/en/fr. Host apps supply routing/display/scope pickers, not a forked state machine. | 2026-07-31 | done | affa5ac, 5e87315, a12c9c6, c7100b1, 95b69d4, 974e24c, 4369400 | ucm-side mirror of the canonical dcm `MSG-3` row. Envelope: [work-orders/MSG-3.md](work-orders/MSG-3.md). Tier 2, 5 proposed chunks (register's "3-5" treated as a floor — jg's parity target is ~3700 LOC excluding tests, `Thread.jsx` alone 2470). Two operator decisions 2026-07-31: (1) **redesign permitted** for layout/composition/interaction, but the design's "No jg feature is lost" stays binding and the WO must ship a written **deviation list** — consequence: MSG-5 becomes a visible change for jg users and needs its own UX review; (2) validation runs through the **DX-1** harness, which is therefore a precondition. Carries `ui_reviewer` at WO end in addition to the per-chunk independent `reviewer`. **Decomposition is a binding requirement, not a preference:** jg's UI is one 2470-LOC component with a 2173-LOC test file, and reproducing that shape is a scope violation even with every feature present — each named component is separately exported, independently mountable and independently testable, state lives in the provider rather than being prop-drilled through `Thread`, every component gets its own DX-1 harness entry (the forcing function — a monolith cannot be mounted piecewise), and any component file above ~400 LOC must be justified in that chunk's review. Depends on dcm 2.36.1 (MSG-2 + MSG-2b). **All 5 chunks implemented, independently reviewed and committed** (chunk 1: additive Layer-1 `onReconnect` extension per operator decision, provider/cache/transport; chunk 2: conversation list + launchers, R1 menu-anchor fix, R2 per-kind icon, R3 broadcast-launcher visibility; chunk 3: Thread/MessageBubble/ReadTicks, R1 N+1 ReadTicks gating fix; chunk 4: Composer/attachments, R1 `extractApiErrorMessage` for dcm's `{files:[...]}` shape, R2 optimistic sender, R3 own-message pending-ReadTicks fix, R4 i18n diacritics; chunk 5: reactions/polls/scope-config/announcements, R1-R4 + a follow-up verification pass V1-V3 including building jg's previously-missing `AnnouncementDialog` parity per explicit operator decision). **WO-end `ui_reviewer`** (independent, full assembled feature not one chunk's diff): found U1-U5, all P2/P3, none blocking — raw-`<button>` reply action instead of MUI `Button` (U1), PollCard packed into the same wrapping flex row as reaction chips instead of its own full-width row (U2), MessagingScopeConfig's text-only loading state instead of the package's `CircularProgress` convention (U3), a raw inline-style attachment thumbnail instead of theme spacing (U4), and `MessageBubble`/`ConversationLaunchers` missing their required DX-1 harness entries (U5). All five fixed by the orchestrator (`974e24c`), re-verified 139/139 tests + clean build. Translation coverage, dark-mode tokens, deviation-doc accuracy and primitive reuse checked clean, no findings. Published: **ucm 2.16.0** (npm, CI-gated on push to `main`, `4369400`). No CHANGELOG.md in this repo (same as noted in NOTIF-13/NOTIF-12) — documented via commit messages + this register entry only. No app pin bumps in this WO. |
|
|
33
33
|
| MSG-3b | Close the MSG-3 parity and contract gaps | Capability-checklist WO closing what MSG-3 missed: the entire unread lifecycle (mark-read never called — badges once raised never clear), message edit/delete + action menu, the DM launcher (`createDirectConversation` had no caller — the case MSG-2b unblocked in the backend), broken reply grouping (`serialize_message` emits `reply_to_id`, `Thread.jsx:40,62` reads `reply_to`, so every server-loaded reply renders as a root), quoted reply previews + jump-to-original, copy, client-side image compression, staged-image previews, upload progress, composer emoji insertion, relative timestamps, active-row highlight, announcement deep-link rendering. Plus a contract-conformance test and a corrected deviation list. | 2026-07-31 | done | c164db3, db8cd92, 61ed3b8, 7253172, fadb60a, 646fd01, 9748aca | Extends the landed MSG-3 (2.16.0), convention as MSG-2b/NOTIF-8b. Envelope: [work-orders/MSG-3b.md](work-orders/MSG-3b.md). Tier 2. **Origin: a three-way audit** (jg capability baseline / ucm wired-vs-adapter-only inventory / dcm contract-coverage sweep) run after MSG-3 was marked done. MSG-3 had 139/139 green, five per-chunk reviews and a clean `ui_reviewer`, and still missed all of it — **because both sides were tested against themselves**: dcm against its services, ucm against a mock whose surface mirrors the provider rather than the contract, so an endpoint with no caller is structurally invisible. Second cause: "no jg feature is lost" was made binding without anyone writing the feature list first, and the deviation list meant to prove parity was authored by the party doing the work; its "Final parity confirmation" section claims completeness and is demonstrably false. This WO's checklist IS the spec (59 rows, each OK/DEV/MISSING/BROKEN/PARTIAL/BLOCKED; the review corrected one false OK — row 5, unread never live-updates at all); the contract-conformance test is the systemic fix. **Rows 38, 51-53, 56-58 are BLOCKED on dcm** (`MSG-2c`, not yet cut): no `serialize_poll` exists at all so `PollCard` reads fields no endpoint returns — and the design specifies no poll read path either, so `messaging-platform.md` must be amended first; `services.py` has 3 `_publish` call sites for 12 designed frames, so reaction/poll live sync cannot work; `serialize_conversation` omits `last_message`, so the list preview is permanently empty. Carries `ui_reviewer` at WO end, which must check the corrected deviation list against the checklist row by row. **All 6 chunks implemented, independently reviewed and committed** (chunk 1: unread lifecycle end-to-end + contract-conformance test, own-fix for a markThreadRead failure silently blocking thread-open; chunk 2: `reply_to`/`reply_to_id` fix + quoting/jump-to-original, row 27 found and recorded BLOCKED (no dcm thread-receipt read path) after an independent-review P1 caught it being silently dropped first time; chunk 3: message action menu/edit/delete/copy with a host-supplied moderator capability prop (dcm exposes no server-side capability field), fixed an undefined-vs-undefined author-match bug; chunk 4: `DirectMessageLauncher` wiring `createDirectConversation`, fixed jest-dom-matcher and MUI-dialog-transition-timing test issues after Codex's own verification pass died without a `RESULT:` line; chunk 5: composer shortcuts/previews/compression/emoji, fixed Codex settling for indeterminate-only upload progress by threading real `onUploadProgress` through the adapter as the WO asked, fixed missing diacritics in new translations (same bug class caught once before in MSG-3), corrected an unverified poll/attachment-mutual-exclusivity claim before it could land in the deviation doc; chunk 6: list/timeline polish (relative timestamps, auto-scroll, announcement links, DM sender-suppression, read-receipt popover) plus the full deviation-doc rewrite against all 59 checklist rows — fixed a repeat of an already-once-fixed raw-interactive-element pattern, corrected an overclaiming doc line, fixed a stale-ref edge case in auto-scroll). Row 42 (managed all-vs-team) also verified genuinely BLOCKED — dcm has `external_key` but never serializes it. **WO-end `ui_reviewer`** (independent, row-by-row cross-check of `docs/messaging-deviations.md` against landed code for all 59 checklist rows, per the WO's binding "the review that signed off the false completeness claim is the one this WO exists to correct" instruction): no findings — every OK/DEV/BLOCKED row verified against actual code, not against the doc's own claims; translation coverage, DX-1 harness coverage, design-system/dark-mode/reuse all checked clean. Published: **ucm 2.17.0** (npm, CI-gated on push to `main`, `9748aca`). No CHANGELOG.md in this repo (same as NOTIF-12/NOTIF-13/MSG-3) — documented via commit messages + this register entry only. No app pin bumps in this WO. |
|
|
34
34
|
| MSG-3c | Deliver the rows MSG-3b left blocked against a stale dcm version | Seven checklist rows (38, 51-53, 56-58) that MSG-3b correctly refused to fake against dcm 2.36.1, and which dcm 2.37.0 made buildable **while MSG-3b was still running**: real last-message preview + list reorder/preview refresh, full `PollCard` rendering (question / options / `vote_count` / `voters` / closed state), and the `reaction` / `poll_updated` frame handlers that stop being dead code. Plus the deviation-doc correction and a conformance-test extension requiring every BLOCKED entry to carry a dependency version pin. | 2026-07-31 | done | 84cc3c2, 5dd4d3a, 733588a, 170ce1e | Envelope: [work-orders/MSG-3c.md](work-orders/MSG-3c.md); canonical row in dcm's register. Extends the landed MSG-3b (2.17.0). **Root cause — third occurrence of one pattern in this workstream:** the WO-end `ui_reviewer` cross-checked all 59 rows against *ucm's* landed code and took the dependency's state from the WO text instead of reading dcm, so seven rows closed as "blocked on a future dcm work order" that had already shipped. New rule this WO adds: a BLOCKED row is re-verified against the live dependency **at WO close, not at WO start**, and every BLOCKED entry names the version it was blocked at. MSG-3b already did the version-pinning unprompted — that pin is the only reason the staleness was a two-minute check rather than surfacing at MSG-5. Rows 27 and 42 stay correctly BLOCKED, on dcm `MSG-2d`. Highest latent risk: rows 56/57 are handler paths that have **never** run against a real frame, so treat them as new code under test rather than as working code being unblocked; and poll tests must use server-shaped payloads (`serialize_poll` output), since client-shaped fixtures are what let the `reply_to_id` bug survive MSG-3. **Codex was out of credits for this entire WO** (verified: every invocation returned only a workspace-credits error, no work done) — all three chunks implemented directly by the orchestrator per the Codex→Claude fallback rule, with mandatory independent review on every chunk (author ≠ reviewer). **Chunk 1** (rows 38/58): fixed a real bug found while scoping — `ConversationList` read `last_message.body`, but dcm's `serialize_last_message` only ever returns `.excerpt` — plus a defensive fix to `applyFrame`'s `message_edited` keying (reviewer found my initial bug analysis incomplete: dcm always attaches a full `frame.message`, so the fix is a no-op against the real wire contract but kept as genuine defensive robustness; the regression test was rewritten to use the real enriched frame shape instead of a fabricated minimal one). **Chunk 2** (rows 51-53): fixed `PollCard` reading non-existent `created_by`/`selected`/`voted` fields (real fields: `created_by_id`, `voted_option_ids` — the latter present only on the vote/create/close REST responses, never in `serialize_poll` or the `poll_updated` frame); found and fixed the same "frame wholesale-replaces viewer-specific state" defect class in `poll_updated`'s own handler ahead of chunk 3's explicit scope. **Chunk 3** (rows 56/57 + WO deliverables): found and fixed the identical defect in the `reaction` frame handler and `toggleReaction`'s REST-confirm path (dcm's `serialize_reactions` never returns a per-viewer `reacted` flag; both call sites were wholesale-replacing and silently wiping known reacted state); extended the contract-conformance test with the WO's new BLOCKED-version-pin rule plus a required negative-case test; rewrote `docs/messaging-deviations.md` (rows 38/51/53/56-58 → OK, row 52 → DEV, rows 27/42 stay BLOCKED with the new version pin, substance untouched). **WO-end `ui_reviewer`** (independent, re-derived rows 27/42 against dcm's actual committed `HEAD`, not the WO text or the doc's own claims, per this WO's own corrective rule): no findings — dcm's committed state is still 2.37.0 (a naive check of dcm's working tree showed 2.38.0, but that's uncommitted `MSG-2d` WIP from a concurrent session, not the published state); rows 27/42 confirmed genuinely still BLOCKED at the real committed version; all seven newly-delivered rows spot-checked against dcm's committed serializers/services directly, all match. Flagged for awareness, not actionable now: dcm's uncommitted `MSG-2d` WIP (external_key, thread-receipt read path) will very likely unblock rows 27/42 once committed and published — a future `MSG-3d` candidate. Published: **ucm 2.18.0** (npm, CI-gated on push to `main`, `170ce1e`). No CHANGELOG.md in this repo (same as MSG-3/MSG-3b/NOTIF-12/NOTIF-13) — documented via commit messages + this register entry only. No app pin bumps in this WO. |
|
|
35
|
-
| MSG-3d | The last two blocked rows: unread-reply marker and managed-conversation identity | Deliver checklist rows **27** (unread-reply marker on the thread toggle, from `reply_count`/`last_reply_at` plus the REST-only `thread_last_read_at`) and **42** (surface `external_key` so a host can distinguish managed conversations — ucm must NOT know that `event_all`/`event_team` mean anything, that is jg vocabulary). Deviation doc reaches **zero** BLOCKED entries. | 2026-08-01 |
|
|
35
|
+
| MSG-3d | The last two blocked rows: unread-reply marker and managed-conversation identity | Deliver checklist rows **27** (unread-reply marker on the thread toggle, from `reply_count`/`last_reply_at` plus the REST-only `thread_last_read_at`) and **42** (surface `external_key` so a host can distinguish managed conversations — ucm must NOT know that `event_all`/`event_team` mean anything, that is jg vocabulary). Deviation doc reaches **zero** BLOCKED entries. | 2026-08-01 | done | 6708ec8, 0701489, f614277 | Envelope: [work-orders/MSG-3d.md](work-orders/MSG-3d.md); canonical row in dcm's register. Extends MSG-3c (2.18.0). Unblocked by dcm 2.38.0 (MSG-2d), verified in `serializers.py` before authoring rather than assumed. **Carries a named trap:** `thread_last_read_at` is viewer-specific and REST-only while `reply_count`/`last_reply_at` ride `serialize_message` and therefore the `message`/`message_edited` frames — so every such frame carries the projection *without* the receipt. That is the **fourth** field of the exact shape whose merge behaviour caused three live bugs in MSG-3c; the field-wise-merge obligation is now in design §Realtime and has a mandatory test here. Second named risk: jg vocabulary leaking into a consumer-agnostic package via row 42, with its own assertion. **Tier 2** (raised from Tier 1 on review — the change lands in the frame-merge code where MSG-3c found three shipped bugs; size does not lower review sensitivity), independent `reviewer` + `ui_reviewer` at WO end. Review also surfaced a plan gap now closed in the envelope: a new reply arrives as its own `message` frame and does NOT update the root's counters, so the root must be incremented client-side or the marker only appears after a refetch (jg does this live). **Closes the post-audit remediation** — after this every MSG-3b checklist row is implemented or a recorded deliberate deviation, and MSG-5 is unblocked. **Codex out of credits again** (fourth consecutive WO in this workstream) — implemented directly, independent review mandatory and run. Fixed a real bug while implementing: `markReplyThreadRead` fired the REST read call and discarded the response entirely, so the receipt was never actually cached client-side before this WO. Own review caught a TDZ crash (a `useCallback` dependency array referencing `patchMessage` before its declaration — dependency arrays evaluate eagerly, unlike the deferred callback body) before the independent reviewer ever saw it. Independent `reviewer`: R1 (P2) — the live-root-update test only proved the marker's visibility, which can't distinguish "still read" from "receipt silently wiped"; added a dedicated assertion on the receipt's own value across the bump, the WO's named highest-risk path. R2 (P2) — the marker's `aria-label` sat on MUI's `Badge` wrapper, which spreads unrecognized props onto a non-interactive `span` a screen reader wouldn't focus; moved onto the inner `Button`. R3 (P3) — a dead client-shaped `reply_to` fallback had crept into the new root-bump code despite the WO's explicit warning against it; removed. R4 (P3) — documented in code that the deliberate absence of self-send-marker suppression relies on dcm excluding the sender from live WS fan-out, an unstated cross-repo invariant. **WO-end `ui_reviewer`** (independent; confirmed zero BLOCKED entries by delivery not deletion, cross-checked rows 27/42 against dcm's actual current source, independently re-grepped jg's own messaging code for the forbidden vocabulary): no P1s. Two P2 harness gaps — `messagingHarnessApi` in `dev/entries.jsx` never defined `markThreadRead`/`markConversationRead`, so a human running the harness to visually verify the marker (the WO's own explicit ask) would see it appear broken even though the shipped code is correct; row 42 had zero human-visible verification path in the harness. Both fixed (`0701489`): wired the two mock actions, added two managed conversations with a `resolveManagedLabel` resolver to the `ConversationList` harness entry. CI's publish run hit the pre-existing `NotificationsProvider.test.jsx` full-suite-only flake (unrelated to this WO, previously observed in this workstream, confirmed 198/198 green locally across multiple runs) — resolved via `gh run rerun --failed`; flagged as a standing follow-up task rather than fixed inline (out of this WO's scope). Published: **ucm 2.19.0** (npm, CI-gated on push to `main`, `f614277`). No CHANGELOG.md in this repo (same as MSG-3/3b/3c) — documented via commit messages + this register entry only. No app pin bumps in this WO. **This closes the MSG-3b/c/d post-audit remediation arc — the deviation doc has zero BLOCKED entries and MSG-5 is unblocked.** |
|
|
36
|
+
| MSG-3e | Per-candidate scope in `DirectMessageLauncher` | Optional `candidates[n].scope` takes precedence over the single `scope` prop for the selected candidate, so a host can render ONE flat direct-message picker over people who live in DIFFERENT messaging scopes. Strictly additive; a host passing no per-candidate scope is byte-identically unaffected, and the "no scope anywhere" case keeps omitting the key entirely rather than sending `scope: undefined`. | 2026-08-02 | planned | | Envelope + map: [work-orders/MSG-3e.md](work-orders/MSG-3e.md). Driver is jg-ferien MSG-6 (global inbox across all of a user's events; a jg DM is event-scoped, so a flat "everyone I may DM" list carries a different scope per person). Alternative was a ~90-line jg-local copy of the component — rejected under AGENTS.md reuse-before-reimplement; the need is generic to any multi-scope host. **Tier 2** because shared-core is excluded from the Tier-1 fast path regardless of diff size, not because the change is large. Named trap: a naive `scope: candidate.scope ?? scope` inside the object literal would start sending `scope: undefined` and break an existing contract. Release 2.20.0 at WO end is the precondition for jg MSG-6's pin bump. |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useId, useState } from 'react';
|
|
2
|
+
import { useId, useMemo, useState } from 'react';
|
|
3
3
|
import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, List, ListItemButton, ListItemText, Typography, useMediaQuery, } from '@mui/material';
|
|
4
4
|
import { useTheme } from '@mui/material/styles';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -18,6 +18,7 @@ export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
|
18
18
|
const [starting, setStarting] = useState(false);
|
|
19
19
|
const [error, setError] = useState(null);
|
|
20
20
|
const titleId = useId();
|
|
21
|
+
const selectedCandidate = useMemo(() => candidates.find((candidate) => candidate.id === selectedId) || null, [candidates, selectedId]);
|
|
21
22
|
const showPicker = () => {
|
|
22
23
|
setSelectedId(null);
|
|
23
24
|
setError(null);
|
|
@@ -28,12 +29,14 @@ export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
|
28
29
|
setOpen(false);
|
|
29
30
|
};
|
|
30
31
|
const start = async () => {
|
|
32
|
+
var _a;
|
|
31
33
|
if (selectedId == null || starting)
|
|
32
34
|
return;
|
|
33
35
|
setStarting(true);
|
|
34
36
|
setError(null);
|
|
35
37
|
try {
|
|
36
|
-
const
|
|
38
|
+
const effectiveScope = (_a = selectedCandidate === null || selectedCandidate === void 0 ? void 0 : selectedCandidate.scope) !== null && _a !== void 0 ? _a : scope;
|
|
39
|
+
const conversation = await openDirectConversation(Object.assign({ target_user_id: selectedId }, (effectiveScope == null ? {} : { scope: effectiveScope })));
|
|
37
40
|
setOpen(false);
|
|
38
41
|
onOpen === null || onOpen === void 0 ? void 0 : onOpen(conversation);
|
|
39
42
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useId, useState } from 'react';
|
|
1
|
+
import { useId, useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Alert,
|
|
4
4
|
Button,
|
|
@@ -32,6 +32,10 @@ export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
|
32
32
|
const [starting, setStarting] = useState(false);
|
|
33
33
|
const [error, setError] = useState(null);
|
|
34
34
|
const titleId = useId();
|
|
35
|
+
const selectedCandidate = useMemo(
|
|
36
|
+
() => candidates.find((candidate) => candidate.id === selectedId) || null,
|
|
37
|
+
[candidates, selectedId],
|
|
38
|
+
);
|
|
35
39
|
|
|
36
40
|
const showPicker = () => {
|
|
37
41
|
setSelectedId(null);
|
|
@@ -46,9 +50,10 @@ export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
|
46
50
|
setStarting(true);
|
|
47
51
|
setError(null);
|
|
48
52
|
try {
|
|
53
|
+
const effectiveScope = selectedCandidate?.scope ?? scope;
|
|
49
54
|
const conversation = await openDirectConversation({
|
|
50
55
|
target_user_id: selectedId,
|
|
51
|
-
...(
|
|
56
|
+
...(effectiveScope == null ? {} : { scope: effectiveScope }),
|
|
52
57
|
});
|
|
53
58
|
setOpen(false);
|
|
54
59
|
onOpen?.(conversation);
|
|
@@ -55,6 +55,47 @@ describe('DirectMessageLauncher', () => {
|
|
|
55
55
|
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it('uses the selected candidate scope over the launcher scope', () => {
|
|
59
|
+
const api = { createDirectConversation: vi.fn().mockResolvedValue({ id: 41, kind: 'direct' }) };
|
|
60
|
+
renderLauncher({
|
|
61
|
+
api,
|
|
62
|
+
candidates: [
|
|
63
|
+
{ id: 2, display_name: 'Alex', scope: 'scope-a' },
|
|
64
|
+
{ id: 3, display_name: 'Sam', scope: 'scope-b' },
|
|
65
|
+
],
|
|
66
|
+
scope: 'scope-prop',
|
|
67
|
+
});
|
|
68
|
+
openPicker();
|
|
69
|
+
fireEvent.click(screen.getByText('Sam'));
|
|
70
|
+
fireEvent.click(screen.getByRole('button', { name: 'MessagingDirect.START' }));
|
|
71
|
+
expect(api.createDirectConversation).toHaveBeenCalledWith({ target_user_id: 3, scope: 'scope-b' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('falls back to the launcher scope when the selected candidate has none', () => {
|
|
75
|
+
const api = { createDirectConversation: vi.fn().mockResolvedValue({ id: 41, kind: 'direct' }) };
|
|
76
|
+
renderLauncher({
|
|
77
|
+
api,
|
|
78
|
+
candidates: [
|
|
79
|
+
{ id: 2, display_name: 'Alex' },
|
|
80
|
+
{ id: 3, display_name: 'Sam', scope: 'scope-b' },
|
|
81
|
+
],
|
|
82
|
+
scope: 'scope-prop',
|
|
83
|
+
});
|
|
84
|
+
openPicker();
|
|
85
|
+
fireEvent.click(screen.getByText('Alex'));
|
|
86
|
+
fireEvent.click(screen.getByRole('button', { name: 'MessagingDirect.START' }));
|
|
87
|
+
expect(api.createDirectConversation).toHaveBeenCalledWith({ target_user_id: 2, scope: 'scope-prop' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('omits scope when neither the candidate nor launcher provides one', () => {
|
|
91
|
+
const api = { createDirectConversation: vi.fn().mockResolvedValue({ id: 41, kind: 'direct' }) };
|
|
92
|
+
renderLauncher({ api });
|
|
93
|
+
openPicker();
|
|
94
|
+
fireEvent.click(screen.getByText('Alex'));
|
|
95
|
+
fireEvent.click(screen.getByRole('button', { name: 'MessagingDirect.START' }));
|
|
96
|
+
expect(Object.keys(api.createDirectConversation.mock.calls[0][0])).toEqual(['target_user_id']);
|
|
97
|
+
});
|
|
98
|
+
|
|
58
99
|
it('surfaces a policy rejection without closing the picker', async () => {
|
|
59
100
|
const api = { createDirectConversation: vi.fn().mockRejectedValue({ response: { data: { detail: 'Direct messages are restricted.' } } }) };
|
|
60
101
|
renderLauncher({ api });
|
package/work-orders/DX-1.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# DX-1 — minimal Vite dev harness for ucm components
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: done · **Tier 2** (dev-only tooling; **adds devDependencies → approval-gated**) · Target repo: `ui-core-micha` (main)
|
|
4
|
+
*Reclassified 2026-08-01: labelled Tier 1 at authoring, but it adds dependencies in a shared-core
|
|
5
|
+
repo — two binding Tier-2 surfaces per AGENTS.md's Tiering gate. A named independent `reviewer` did
|
|
6
|
+
run (one P2 on the provider's own WebSocket), so only the label was wrong.*
|
|
4
7
|
|
|
5
8
|
---
|
|
6
9
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# MSG-3e — per-candidate scope in `DirectMessageLauncher`
|
|
2
|
+
|
|
3
|
+
Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Part A — Envelope (2026-08-02)
|
|
8
|
+
|
|
9
|
+
### Goal
|
|
10
|
+
|
|
11
|
+
Let a host present **one flat direct-message picker whose candidates live in different messaging
|
|
12
|
+
scopes**. Today `DirectMessageLauncher` takes a single `scope` prop and applies it to every candidate,
|
|
13
|
+
so a host with people spread across several scopes must either render one launcher per scope or
|
|
14
|
+
re-implement the component.
|
|
15
|
+
|
|
16
|
+
### Why now
|
|
17
|
+
|
|
18
|
+
jg-ferien MSG-6 turns `/messages` into a global inbox across **all** of a user's events. In jg's model a
|
|
19
|
+
direct conversation is scoped to an event, so a flat "everyone I may DM" list carries a **different
|
|
20
|
+
scope per person** — the operator's explicit requirement ("DM über alle, Gruppen pro Event"). Without
|
|
21
|
+
this seam jg would have to copy ~90 lines of this component locally, which AGENTS.md's
|
|
22
|
+
reuse-before-reimplement rule exists to prevent. The need is generic: any host whose people span
|
|
23
|
+
multiple scopes hits it.
|
|
24
|
+
|
|
25
|
+
### Expected outcome
|
|
26
|
+
|
|
27
|
+
`candidates[n].scope` (optional) takes precedence over the `scope` prop **for the selected candidate
|
|
28
|
+
only**. Precisely:
|
|
29
|
+
|
|
30
|
+
| selected candidate's `scope` | `scope` prop | scope sent to `createDirectConversation` |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| present | present | the candidate's |
|
|
33
|
+
| present | absent | the candidate's |
|
|
34
|
+
| absent | present | the prop's (today's behaviour) |
|
|
35
|
+
| absent | absent | no `scope` key in the payload at all (today's behaviour) |
|
|
36
|
+
|
|
37
|
+
Strictly additive and backward compatible: a host that passes no per-candidate scope sees byte-identical
|
|
38
|
+
behaviour. Note the last row — the current code omits the key entirely rather than sending
|
|
39
|
+
`scope: null`, and that must not change; dcm distinguishes the two.
|
|
40
|
+
|
|
41
|
+
### Scope / non-goals
|
|
42
|
+
|
|
43
|
+
In scope: `src/messaging/DirectMessageLauncher.jsx` and its test file.
|
|
44
|
+
|
|
45
|
+
Explicit non-goals — do **not**:
|
|
46
|
+
- change the dialog's layout, copy, i18n keys, or a11y semantics;
|
|
47
|
+
- add grouping, sectioning, or per-candidate secondary text (a scope is not a label — jg groups its
|
|
48
|
+
*group* picker by event locally, and that stays a host concern);
|
|
49
|
+
- touch `MessagingProvider`'s `openDirectConversation` signature or any other component;
|
|
50
|
+
- validate or interpret the scope value — it is an opaque host-supplied identifier, passed through.
|
|
51
|
+
|
|
52
|
+
### Tier
|
|
53
|
+
|
|
54
|
+
Tier 2. Shared-core (`ui-core-micha`) is excluded from the Tier-1 fast path by AGENTS.md regardless of
|
|
55
|
+
diff size, so this one-line-of-logic change gets a WO, Codex-first, and a mandatory independent review.
|
|
56
|
+
|
|
57
|
+
### Tests to WRITE (scoped — run only these)
|
|
58
|
+
|
|
59
|
+
Extend `tests/messagingDirectMessageLauncher.test.jsx`:
|
|
60
|
+
|
|
61
|
+
1. **per-candidate scope wins** — candidates `[{id: 2, display_name: 'Alex', scope: 'scope-a'}, {id: 3,
|
|
62
|
+
display_name: 'Sam', scope: 'scope-b'}]`, launcher `scope="scope-prop"`; select Sam, start; assert
|
|
63
|
+
`createDirectConversation` called with `{target_user_id: 3, scope: 'scope-b'}`.
|
|
64
|
+
2. **fallback to the prop** (regression) — a candidate without its own `scope` in the same mixed list
|
|
65
|
+
still sends the prop's scope.
|
|
66
|
+
3. **neither present** — no `scope` key in the payload (`expect(Object.keys(payload)).toEqual(
|
|
67
|
+
['target_user_id'])` or equivalent — asserting absence, not `undefined`).
|
|
68
|
+
|
|
69
|
+
The three existing tests in that file must keep passing unchanged; they are the backward-compatibility
|
|
70
|
+
proof and must not be edited to accommodate the change.
|
|
71
|
+
|
|
72
|
+
Command: `pnpm vitest run tests/messagingDirectMessageLauncher.test.jsx`
|
|
73
|
+
|
|
74
|
+
### Release
|
|
75
|
+
|
|
76
|
+
One version bump `2.19.0` → **`2.20.0`** (minor: additive, backward-compatible) + npm publish at WO end.
|
|
77
|
+
jg-ferien MSG-6 pins this version, so the publish is the precondition for that WO's final test run.
|
|
78
|
+
Per `reference_dcm_ucm_publish_topology`: publish-from-main, and the target version must be verified
|
|
79
|
+
live on the registry before jg bumps its pin.
|
|
80
|
+
|
|
81
|
+
### Risks
|
|
82
|
+
|
|
83
|
+
Low. The one real trap is the "absent/absent" row above — a naive `scope: candidate.scope ?? scope`
|
|
84
|
+
inside the object literal would start sending `scope: undefined`, which serialises differently and
|
|
85
|
+
changes an existing contract. Build the payload conditionally, as the current code already does.
|
|
86
|
+
|
|
87
|
+
### Execution directive
|
|
88
|
+
|
|
89
|
+
**If you are the implementer reading this as your own spec, this section is not addressed to you — it
|
|
90
|
+
tells the Orchestrator how to invoke you. You ARE that invocation: do not shell out to `codex exec`.**
|
|
91
|
+
|
|
92
|
+
Orchestrator: implement through `codex exec` in the background, invoked **directly via Bash** (never the
|
|
93
|
+
`debugger`/`*_coder` Agent wrappers), with **both** flags `--skip-git-repo-check` and
|
|
94
|
+
`--dangerously-bypass-approvals-and-sandbox`, the WO passed as a positional argument from this committed
|
|
95
|
+
file. Fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit; if the
|
|
96
|
+
fallback is used the Orchestrator becomes author and an independent `reviewer` is mandatory.
|
|
97
|
+
|
|
98
|
+
### Mini-handover (pastable)
|
|
99
|
+
|
|
100
|
+
Orchestrator: implement `work-orders/MSG-3e.md` in `ui-core-micha` (main). `git pull` first, read the WO,
|
|
101
|
+
then follow `orchestrate-codex` (Codex-first, own independent review, publish 2.20.0 at WO end).
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Part B — Implementation map (Orchestrator)
|
|
106
|
+
|
|
107
|
+
**Single chunk.** **Target repo working directory:** `C:\Users\Micha Bigler\Documents\webapps\ui-core-micha`
|
|
108
|
+
(repo root). Work from this package; open only the named files to verify.
|
|
109
|
+
|
|
110
|
+
### The one file that changes
|
|
111
|
+
|
|
112
|
+
`src/messaging/DirectMessageLauncher.jsx` — 96 lines, whole file is in scope but only `start()` and one
|
|
113
|
+
derivation change.
|
|
114
|
+
|
|
115
|
+
Current state (verified while scoping, do not re-derive):
|
|
116
|
+
|
|
117
|
+
- `:25` — `export function DirectMessageLauncher({ candidates = [], scope, onOpen })`.
|
|
118
|
+
- `:31` — the component tracks **only** `selectedId`, never the selected candidate object. A lookup is
|
|
119
|
+
therefore needed; mirror the pattern jg already uses in its own `AllGroupsLauncher`
|
|
120
|
+
(`jg-ferien/frontend/src/pages/messaging/AllGroupsLauncher.jsx:40-44`):
|
|
121
|
+
`const selectedCandidate = useMemo(() => candidates.find((c) => c.id === selectedId) || null,
|
|
122
|
+
[candidates, selectedId]);` — `useMemo` is not yet imported in this file (`:1` imports `useId, useState`).
|
|
123
|
+
- `:44-60` — `start()`; the payload is built at `:49-52`:
|
|
124
|
+
```js
|
|
125
|
+
const conversation = await openDirectConversation({
|
|
126
|
+
target_user_id: selectedId,
|
|
127
|
+
...(scope == null ? {} : { scope }),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
This spread-or-nothing shape is the invariant the third test pins: when there is no scope the key is
|
|
131
|
+
**absent**, not `undefined`. Keep the shape; only the value fed into it changes:
|
|
132
|
+
`const effectiveScope = selectedCandidate?.scope ?? scope;` then `...(effectiveScope == null ? {} :
|
|
133
|
+
{ scope: effectiveScope })`. `??` (not `||`) so a falsy-but-valid scope id survives.
|
|
134
|
+
- `:75-82` — the `ListItemButton` render loop; **do not** change it. Candidate rendering already falls
|
|
135
|
+
back through `display_name || name || label || id` and the scope must not appear in the UI.
|
|
136
|
+
|
|
137
|
+
### Invariants / do-not-touch
|
|
138
|
+
|
|
139
|
+
- `extractApiErrorMessage` error path (`:55-59`) and the `starting` guard stay exactly as they are.
|
|
140
|
+
- `openDirectConversation` comes from `useMessaging()` (`:19, :27`) — its signature is dcm's contract,
|
|
141
|
+
unchanged.
|
|
142
|
+
- `src/index.js:72` already exports the component; no export change.
|
|
143
|
+
- No new i18n keys (`MessagingDirect.*` set is complete for this change).
|
|
144
|
+
|
|
145
|
+
### Docs
|
|
146
|
+
|
|
147
|
+
`docs/messaging-deviations.md` currently has zero BLOCKED entries (MSG-3d closed them) — this WO adds no
|
|
148
|
+
deviation and must not add a row. If the repo carries a prop-level API note for this component, update it;
|
|
149
|
+
if not, do not invent a docs file.
|
|
150
|
+
|
|
151
|
+
### Progress contract
|
|
152
|
+
|
|
153
|
+
Emit a `PLAN: …` line first, then a single-line `PROGRESS: [n/total] <action>` before every relevant
|
|
154
|
+
action and `… done` on completion, no gap > ~2 min, unbuffered stdout, and exactly one final
|
|
155
|
+
`RESULT: DONE|BLOCKED <reason>`.
|
|
156
|
+
|
|
157
|
+
### Constraints for the implementer
|
|
158
|
+
|
|
159
|
+
The WO text is the COMPLETE spec. Read `AGENTS.md` and `.codex/skills/frontend-engineering` only for
|
|
160
|
+
conventions. Stay in scope. Do not touch the Core-Behaviour approval surfaces. Do not update `MEMORY.md`.
|
|
161
|
+
Do **not** `git add/commit/push` — leave the diff for the Orchestrator's review. Do **not** bump the
|
|
162
|
+
version or publish — the Orchestrator owns the release. WRITE the three tests above and RUN ONLY that one
|
|
163
|
+
test file; no full suite, no self-review — the Orchestrator owns the authoritative run and the
|
|
164
|
+
independent review, and those are the gate.
|