@micha.bigler/ui-core-micha 2.18.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.
@@ -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.