@micha.bigler/ui-core-micha 3.14.3 → 4.0.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.
Files changed (367) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +7 -0
  3. package/dist/auth/UserMenu.js +1 -1
  4. package/dist/auth/apiClient.js +7 -6
  5. package/dist/chat/ChatList.js +1 -1
  6. package/dist/chat/ChatThread.js +2 -1
  7. package/dist/chat/ChatWorkspace.js +1 -1
  8. package/dist/chat/ModelPicker.js +2 -5
  9. package/dist/chat/kitSxRegistry.js +9 -0
  10. package/dist/chat/useChatSocket.js +1 -1
  11. package/dist/components/LoginForm.js +1 -1
  12. package/dist/components/SecurityComponent.js +1 -1
  13. package/dist/components/UserListComponent.js +2 -8
  14. package/dist/components/charts/ChartFrame.js +1 -1
  15. package/dist/components/charts/ScatterChart.js +2 -1
  16. package/dist/components/charts/kitSxRegistry.js +6 -0
  17. package/dist/entries/charts.js +6 -0
  18. package/dist/entries/chat.js +8 -0
  19. package/dist/entries/markdown.js +1 -0
  20. package/dist/index.js +0 -14
  21. package/dist/layout/SectionNav.js +1 -1
  22. package/dist/messaging/AttachmentList.js +2 -2
  23. package/dist/messaging/Composer.js +2 -1
  24. package/dist/messaging/ConversationLaunchers.js +1 -4
  25. package/dist/messaging/ConversationList.js +1 -1
  26. package/dist/messaging/DirectMessageLauncher.js +1 -1
  27. package/dist/messaging/MessagingProvider.js +0 -0
  28. package/dist/messaging/PollCard.js +7 -5
  29. package/dist/messaging/ReadTicks.js +2 -2
  30. package/dist/messaging/Thread.js +5 -3
  31. package/dist/notifications/NotificationBell.js +4 -7
  32. package/dist/notifications/NotificationSettings.js +1 -1
  33. package/dist/onboarding/OnboardingProvider.js +3 -6
  34. package/dist/pages/AccountPage.js +1 -2
  35. package/dist/pages/LoginPage.js +2 -3
  36. package/dist/pages/PasswordChangePage.js +2 -3
  37. package/dist/pages/PasswordInvitePage.js +2 -3
  38. package/dist/pages/PasswordResetRequestPage.js +2 -3
  39. package/dist/pages/SignUpPage.js +2 -3
  40. package/dist/pages/SignupConfirmPage.js +2 -3
  41. package/dist/theme/createAppTheme.js +5 -4
  42. package/dist/theme/kitSxEntry.js +11 -0
  43. package/dist/theme/kitSxRegistry.js +2 -15
  44. package/dist/theme/themeCompleteness.js +3 -12
  45. package/dist/utils/auth-errors.js +4 -3
  46. package/package.json +41 -16
  47. package/.github/workflows/publish.yml +0 -126
  48. package/DESIGN.md +0 -60
  49. package/WORK_ORDERS.md +0 -148
  50. package/dev/entries.jsx +0 -721
  51. package/dev/fixtures.js +0 -26
  52. package/dev/main.jsx +0 -75
  53. package/dev/mockTransport.jsx +0 -128
  54. package/docs/CHART-LAYOUT.md +0 -157
  55. package/docs/DARK-TOKENS.md +0 -140
  56. package/docs/TYPE-SCALE.md +0 -111
  57. package/docs/messaging-deviations.md +0 -118
  58. package/index.html +0 -12
  59. package/renovate.json +0 -10
  60. package/scripts/chart-api-census.mjs +0 -415
  61. package/src/auth/AuthContext.jsx +0 -158
  62. package/src/auth/ReauthModal.jsx +0 -82
  63. package/src/auth/UserMenu.jsx +0 -121
  64. package/src/auth/apiClient.jsx +0 -232
  65. package/src/auth/authApi.jsx +0 -539
  66. package/src/auth/authConfig.jsx +0 -34
  67. package/src/auth/reauth.js +0 -47
  68. package/src/chat/ChatComposer.jsx +0 -36
  69. package/src/chat/ChatList.jsx +0 -87
  70. package/src/chat/ChatThread.jsx +0 -128
  71. package/src/chat/ChatWorkspace.jsx +0 -244
  72. package/src/chat/MarkdownReply.jsx +0 -22
  73. package/src/chat/ModelPicker.jsx +0 -85
  74. package/src/chat/api.js +0 -18
  75. package/src/chat/useChatSocket.js +0 -32
  76. package/src/chat/wire.js +0 -7
  77. package/src/components/AccessCodeManager.jsx +0 -288
  78. package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
  79. package/src/components/AllowedEmailDomainsManager.jsx +0 -92
  80. package/src/components/AuthFactorRequirementCard.jsx +0 -95
  81. package/src/components/BulkInviteCsvTab.jsx +0 -198
  82. package/src/components/EmptyState.jsx +0 -38
  83. package/src/components/LoginForm.jsx +0 -159
  84. package/src/components/MFAComponent.jsx +0 -350
  85. package/src/components/MfaLoginComponent.jsx +0 -252
  86. package/src/components/MobileBottomNav.jsx +0 -82
  87. package/src/components/PasskeysComponent.jsx +0 -252
  88. package/src/components/PasswordChangeForm.jsx +0 -87
  89. package/src/components/PasswordResetRequestForm.jsx +0 -43
  90. package/src/components/PasswordSetForm.jsx +0 -97
  91. package/src/components/ProfileComponent.jsx +0 -348
  92. package/src/components/QrSignupManager.jsx +0 -404
  93. package/src/components/QrSignupValidityManager.jsx +0 -106
  94. package/src/components/RegistrationMethodsManager.jsx +0 -150
  95. package/src/components/SecurityComponent.jsx +0 -141
  96. package/src/components/SocialLoginButtons.jsx +0 -83
  97. package/src/components/SoftChip.jsx +0 -93
  98. package/src/components/StatTile.jsx +0 -39
  99. package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
  100. package/src/components/TurnstileWidget.jsx +0 -99
  101. package/src/components/UserInviteComponent.jsx +0 -76
  102. package/src/components/UserListComponent.jsx +0 -618
  103. package/src/components/charts/BarChart.jsx +0 -102
  104. package/src/components/charts/ChartFrame.jsx +0 -214
  105. package/src/components/charts/LineChart.jsx +0 -126
  106. package/src/components/charts/ScatterChart.jsx +0 -299
  107. package/src/components/charts/TimeSeriesChart.jsx +0 -300
  108. package/src/components/charts/chartDefaults.js +0 -644
  109. package/src/components/charts/chartLabels.js +0 -36
  110. package/src/components/charts/exportChart.js +0 -258
  111. package/src/components/charts/formatters.js +0 -39
  112. package/src/components/charts/palette.js +0 -33
  113. package/src/components/charts/yearTickInterval.js +0 -7
  114. package/src/i18n/authTranslations.ts +0 -2651
  115. package/src/i18n/chartsTranslations.ts +0 -91
  116. package/src/i18n/chatTranslations.ts +0 -55
  117. package/src/i18n/messagingTranslations.ts +0 -120
  118. package/src/i18n/notificationsTranslations.ts +0 -32
  119. package/src/i18n/onboardingTranslations.ts +0 -29
  120. package/src/i18n/sectionNavTranslations.ts +0 -20
  121. package/src/i18n/uiCoreTranslations.js +0 -29
  122. package/src/i18n/userMenuTranslations.ts +0 -5
  123. package/src/index.js +0 -158
  124. package/src/layout/PageLayout.jsx +0 -36
  125. package/src/layout/SectionNav.jsx +0 -320
  126. package/src/messaging/AttachmentList.jsx +0 -145
  127. package/src/messaging/Composer.jsx +0 -197
  128. package/src/messaging/ConversationLaunchers.jsx +0 -51
  129. package/src/messaging/ConversationList.jsx +0 -109
  130. package/src/messaging/DirectMessageLauncher.jsx +0 -112
  131. package/src/messaging/MessageBubble.jsx +0 -183
  132. package/src/messaging/MessagingProvider.jsx +0 -0
  133. package/src/messaging/MessagingScopeConfig.jsx +0 -24
  134. package/src/messaging/PollCard.jsx +0 -163
  135. package/src/messaging/ReactionBar.jsx +0 -43
  136. package/src/messaging/ReadTicks.jsx +0 -82
  137. package/src/messaging/Thread.jsx +0 -195
  138. package/src/messaging/api.js +0 -48
  139. package/src/messaging/composerImageCompression.js +0 -35
  140. package/src/messaging/linkifyText.jsx +0 -37
  141. package/src/notifications/NotificationBell.jsx +0 -84
  142. package/src/notifications/NotificationSettings.jsx +0 -330
  143. package/src/notifications/NotificationsProvider.jsx +0 -224
  144. package/src/notifications/PopupSurface.jsx +0 -105
  145. package/src/notifications/api.js +0 -55
  146. package/src/notifications/feedApi.js +0 -18
  147. package/src/notifications/realtime.jsx +0 -138
  148. package/src/notifications/serviceWorker/sw.js +0 -50
  149. package/src/onboarding/OnboardingProvider.jsx +0 -326
  150. package/src/onboarding/OnboardingWizard.jsx +0 -61
  151. package/src/onboarding/WizardDialogShell.jsx +0 -51
  152. package/src/onboarding/api.js +0 -13
  153. package/src/onboarding/stepSelection.js +0 -16
  154. package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
  155. package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
  156. package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
  157. package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
  158. package/src/pages/AccountPage.jsx +0 -394
  159. package/src/pages/LoginPage.jsx +0 -343
  160. package/src/pages/PasswordChangePage.jsx +0 -62
  161. package/src/pages/PasswordInvitePage.jsx +0 -177
  162. package/src/pages/PasswordResetRequestPage.jsx +0 -68
  163. package/src/pages/SignUpPage.jsx +0 -268
  164. package/src/pages/SignupConfirmPage.jsx +0 -155
  165. package/src/testing/checkKitIntegration.js +0 -233
  166. package/src/theme/createAppTheme.js +0 -423
  167. package/src/theme/fonts.js +0 -3
  168. package/src/theme/index.js +0 -21
  169. package/src/theme/kitSxRegistry.js +0 -285
  170. package/src/theme/themeCompleteness.js +0 -1045
  171. package/src/theme/tokens.js +0 -478
  172. package/src/utils/auth-errors.js +0 -76
  173. package/src/utils/authService.js +0 -223
  174. package/src/utils/resolveErrorText.js +0 -28
  175. package/src/utils/webauthn.js +0 -51
  176. package/tests/AccountPage.profileStatements.test.jsx +0 -162
  177. package/tests/AccountPage.test.jsx +0 -200
  178. package/tests/AuthContext.test.jsx +0 -129
  179. package/tests/BarChart.test.jsx +0 -112
  180. package/tests/BrowserPushStep.test.jsx +0 -110
  181. package/tests/ChartFrame.test.jsx +0 -336
  182. package/tests/ConversationList.test.jsx +0 -144
  183. package/tests/CookieConsentStep.test.jsx +0 -69
  184. package/tests/EmptyState.test.jsx +0 -62
  185. package/tests/LineChart.test.jsx +0 -80
  186. package/tests/MessagingProvider.test.jsx +0 -50
  187. package/tests/MobileBottomNav.test.jsx +0 -319
  188. package/tests/NotificationBell.test.jsx +0 -60
  189. package/tests/NotificationSettings.test.jsx +0 -276
  190. package/tests/NotificationsProvider.test.jsx +0 -240
  191. package/tests/OnboardingProvider.test.jsx +0 -294
  192. package/tests/OnboardingWizard.test.jsx +0 -143
  193. package/tests/PasswordChangeForm.test.jsx +0 -70
  194. package/tests/PasswordInvitePage.test.jsx +0 -142
  195. package/tests/PasswordSetForm.test.jsx +0 -70
  196. package/tests/PopupSurface.test.jsx +0 -142
  197. package/tests/ProfileComponent.refresh.test.jsx +0 -194
  198. package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
  199. package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
  200. package/tests/ScatterChart.test.jsx +0 -270
  201. package/tests/SectionNav.test.jsx +0 -363
  202. package/tests/SignUpPage.test.jsx +0 -208
  203. package/tests/SoftChip.test.jsx +0 -105
  204. package/tests/StatTile.test.jsx +0 -57
  205. package/tests/Thread.test.jsx +0 -151
  206. package/tests/TimeSeriesChart.test.jsx +0 -367
  207. package/tests/UserListComponent.test.jsx +0 -192
  208. package/tests/UserMenu.test.jsx +0 -171
  209. package/tests/apiClientLanguage.test.js +0 -33
  210. package/tests/apiClientPublicPaths.test.js +0 -178
  211. package/tests/authApiLogin.test.js +0 -42
  212. package/tests/authApiRegistration.test.js +0 -31
  213. package/tests/authErrorCodeRendering.test.jsx +0 -77
  214. package/tests/authErrors.test.js +0 -74
  215. package/tests/authTranslations.test.js +0 -74
  216. package/tests/chart-api-census.test.js +0 -366
  217. package/tests/chartAxisTickBand.test.jsx +0 -95
  218. package/tests/chartDefaults.test.js +0 -902
  219. package/tests/chartLabels.test.js +0 -50
  220. package/tests/chartsPalette.test.js +0 -99
  221. package/tests/chatApi.test.js +0 -48
  222. package/tests/chatDriverNames.test.jsx +0 -71
  223. package/tests/chatExports.test.js +0 -3
  224. package/tests/chatList.test.jsx +0 -84
  225. package/tests/chatPicker.test.jsx +0 -82
  226. package/tests/chatSocket.test.jsx +0 -26
  227. package/tests/chatThread.test.jsx +0 -116
  228. package/tests/chatTranslations.test.js +0 -47
  229. package/tests/chatWorkspace.test.jsx +0 -234
  230. package/tests/checkKitIntegration.test.jsx +0 -219
  231. package/tests/createAppTheme.test.js +0 -132
  232. package/tests/createAppThemeDark.test.js +0 -127
  233. package/tests/createAppThemeLightGuard.test.js +0 -40
  234. package/tests/createAppThemePageContrast.test.js +0 -108
  235. package/tests/createAppThemeTypography.test.js +0 -53
  236. package/tests/errorCallSiteGuard.test.js +0 -32
  237. package/tests/exportChart.test.js +0 -313
  238. package/tests/feedApi.test.js +0 -46
  239. package/tests/fixtures/light-theme-baseline.json +0 -2234
  240. package/tests/i18nAggregate.test.js +0 -120
  241. package/tests/i18nGermanVariant.test.js +0 -65
  242. package/tests/loginPageErrorResolution.test.jsx +0 -102
  243. package/tests/messagingApi.test.js +0 -32
  244. package/tests/messagingChunk6.test.jsx +0 -116
  245. package/tests/messagingCompactBubble.test.jsx +0 -64
  246. package/tests/messagingComposer.test.jsx +0 -136
  247. package/tests/messagingComposerChunk5.test.jsx +0 -113
  248. package/tests/messagingContractConformance.test.js +0 -69
  249. package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
  250. package/tests/messagingExports.test.js +0 -21
  251. package/tests/messagingInteractions.test.jsx +0 -136
  252. package/tests/messagingLastMessagePreview.test.jsx +0 -93
  253. package/tests/messagingLinkify.test.jsx +0 -51
  254. package/tests/messagingMessageActions.test.jsx +0 -163
  255. package/tests/messagingMsg6.test.jsx +0 -105
  256. package/tests/messagingMsg6f.test.jsx +0 -155
  257. package/tests/messagingMsg6g.test.jsx +0 -166
  258. package/tests/messagingMsg6h.test.jsx +0 -136
  259. package/tests/messagingMsg6i.test.jsx +0 -129
  260. package/tests/messagingPollRendering.test.jsx +0 -142
  261. package/tests/messagingReactionMerge.test.jsx +0 -77
  262. package/tests/messagingReadTicksTooltip.test.jsx +0 -36
  263. package/tests/messagingThreadReceipts.test.jsx +0 -178
  264. package/tests/messagingThreadingQuoting.test.jsx +0 -87
  265. package/tests/messagingUnreadLifecycle.test.jsx +0 -78
  266. package/tests/notificationsApi.test.js +0 -41
  267. package/tests/notificationsExports.test.js +0 -34
  268. package/tests/onboardingDescriptors.test.js +0 -132
  269. package/tests/packageTreeShaking.test.js +0 -126
  270. package/tests/realtime.test.jsx +0 -219
  271. package/tests/resolveErrorText.test.js +0 -72
  272. package/tests/stepSelection.test.js +0 -53
  273. package/tests/themeCompleteness.test.js +0 -626
  274. package/tests/timeSeriesLegend.test.js +0 -99
  275. package/tests/userScreenTranslations.test.jsx +0 -218
  276. package/tsconfig.build.json +0 -18
  277. package/vite.config.mjs +0 -6
  278. package/work-orders/AUTH-1.md +0 -74
  279. package/work-orders/AUTH-2.md +0 -74
  280. package/work-orders/AUTH-4.md +0 -183
  281. package/work-orders/AUTH-5.md +0 -154
  282. package/work-orders/AUTH-7.md +0 -181
  283. package/work-orders/AUTH-8.md +0 -184
  284. package/work-orders/CHART-1.md +0 -285
  285. package/work-orders/CHART-2.md +0 -222
  286. package/work-orders/CHART-3.md +0 -163
  287. package/work-orders/CHART-4.md +0 -203
  288. package/work-orders/CHART-5.md +0 -226
  289. package/work-orders/CHART-6.md +0 -428
  290. package/work-orders/CHART-7.md +0 -175
  291. package/work-orders/DX-1.md +0 -169
  292. package/work-orders/DX-2.md +0 -188
  293. package/work-orders/DX-3.md +0 -195
  294. package/work-orders/I18N-1.md +0 -254
  295. package/work-orders/I18N-3.md +0 -207
  296. package/work-orders/MSG-3.md +0 -375
  297. package/work-orders/MSG-3b.md +0 -1002
  298. package/work-orders/MSG-3c.md +0 -378
  299. package/work-orders/MSG-3d.md +0 -268
  300. package/work-orders/MSG-3e.md +0 -164
  301. package/work-orders/MSG-3f.md +0 -391
  302. package/work-orders/MSG-6e.md +0 -308
  303. package/work-orders/MSG-6f.md +0 -214
  304. package/work-orders/MSG-6g.md +0 -244
  305. package/work-orders/MSG-6h.md +0 -238
  306. package/work-orders/MSG-6i.md +0 -271
  307. package/work-orders/MSG-6j.md +0 -117
  308. package/work-orders/NOTIF-12.md +0 -214
  309. package/work-orders/NOTIF-13.md +0 -142
  310. package/work-orders/NOTIF-14.md +0 -151
  311. package/work-orders/PRIM-1.md +0 -177
  312. package/work-orders/SHELL-1.md +0 -255
  313. package/work-orders/SHELL-2.md +0 -197
  314. package/work-orders/SHELL-3.md +0 -359
  315. package/work-orders/SHELL-4.md +0 -351
  316. package/work-orders/SHELL-5.md +0 -345
  317. package/work-orders/SHELL-6.md +0 -210
  318. package/work-orders/THEME-1.md +0 -587
  319. package/work-orders/THEME-10.md +0 -303
  320. package/work-orders/THEME-11.md +0 -208
  321. package/work-orders/THEME-2.md +0 -339
  322. package/work-orders/THEME-3.md +0 -210
  323. package/work-orders/THEME-4.md +0 -271
  324. package/work-orders/THEME-5.md +0 -341
  325. package/work-orders/THEME-6.md +0 -247
  326. package/work-orders/THEME-7.md +0 -82
  327. package/work-orders/THEME-8.md +0 -211
  328. package/work-orders/THEME-9.md +0 -194
  329. package/work-orders/UCM-AUTH-10.md +0 -490
  330. package/work-orders/UCM-AUTH-11.md +0 -185
  331. package/work-orders/UCM-AUTH-12.md +0 -98
  332. package/work-orders/UCM-AUTH-13.md +0 -47
  333. package/work-orders/UCM-AUTH-9.md +0 -207
  334. package/work-orders/UCM-CHART-10.md +0 -150
  335. package/work-orders/UCM-CHART-11.md +0 -132
  336. package/work-orders/UCM-CHART-12.md +0 -348
  337. package/work-orders/UCM-CHART-13.md +0 -122
  338. package/work-orders/UCM-CHART-14.md +0 -198
  339. package/work-orders/UCM-CHART-15.md +0 -187
  340. package/work-orders/UCM-CHART-16.md +0 -169
  341. package/work-orders/UCM-CHART-17.md +0 -195
  342. package/work-orders/UCM-CHART-18.md +0 -200
  343. package/work-orders/UCM-CHART-8.md +0 -147
  344. package/work-orders/UCM-CHART-9.md +0 -265
  345. package/work-orders/UCM-CHAT-1.md +0 -190
  346. package/work-orders/UCM-CHAT-2.md +0 -75
  347. package/work-orders/UCM-DEP-1.md +0 -63
  348. package/work-orders/UCM-I18N-4.md +0 -226
  349. package/work-orders/UCM-I18N-5.md +0 -211
  350. package/work-orders/UCM-MSG-20.md +0 -192
  351. package/work-orders/UCM-PRIM-2.md +0 -114
  352. package/work-orders/UCM-SHELL-7.md +0 -134
  353. package/work-orders/UCM-SHELL-8.md +0 -125
  354. package/work-orders/UCM-TEST-1.md +0 -304
  355. package/work-orders/UCM-THEME-13.md +0 -69
  356. package/work-orders/UCM-THEME-14.md +0 -312
  357. package/work-orders/UCM-THEME-15.md +0 -356
  358. package/work-orders/UCM-THEME-16.md +0 -80
  359. package/work-orders/UCM-THEME-17.md +0 -111
  360. package/work-orders/UCM-THEME-18.md +0 -184
  361. package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
  362. package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
  363. package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
  364. package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
  365. package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
  366. package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
  367. package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
@@ -1,1002 +0,0 @@
1
- # MSG-3b — close the MSG-3 parity and contract gaps
2
-
3
- Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
4
- **Binding spec:** `django-core-micha/docs/design/messaging-platform.md`. Extends the landed MSG-3
5
- (published ucm 2.16.0), same convention as `MSG-2b` / `NOTIF-8b`.
6
-
7
- ---
8
-
9
- ## Part A — Envelope (Expertenchat, 2026-07-31)
10
-
11
- ### Goal
12
-
13
- Close the gaps a three-way audit found after MSG-3 was marked done: capabilities jg has that ucm never
14
- got, capabilities ucm renders but that cannot work against the real backend, and one whole area
15
- (unread lifecycle) that was never specified and therefore never built.
16
-
17
- ### Why this WO exists — read before scoping anything
18
-
19
- MSG-3 shipped with 139/139 tests green, five per-chunk independent reviews and a clean `ui_reviewer`
20
- pass, and still missed all of the below. Two causes, both structural:
21
-
22
- 1. **Both sides were tested against themselves.** dcm tested its services; ucm tested its components
23
- against a mock whose API surface mirrors *the provider*, not *the contract*. An endpoint with no
24
- caller is structurally invisible to that mock. The seam was never executed.
25
- 2. **"No jg feature is lost" was made binding without anyone writing the feature list first**, and the
26
- deviation list that was supposed to prove parity was authored by the same party doing the work.
27
- It claimed completeness and was wrong.
28
-
29
- This WO fixes both: the checklist below **is** the specification, and a contract-conformance test makes
30
- the seam mechanically checkable.
31
-
32
- ### Capability checklist — this is the specification
33
-
34
- Status as of ucm 2.16.0. `OK` = present, no action. `DEV` = deviation already recorded in
35
- `docs/messaging-deviations.md` and deliberately kept. `MISSING` / `BROKEN` / `PARTIAL` = in scope.
36
- `BLOCKED` = needs dcm work first (see Dependencies).
37
-
38
- **Every row not marked `OK` or `DEV` must end this WO either implemented or added to the deviation
39
- list with an explicit rationale. No row may be silently dropped.**
40
-
41
- #### Unread lifecycle — never specified in MSG-3, never built
42
-
43
- | # | Capability | jg reference | Status |
44
- |---|---|---|---|
45
- | 1 | Selecting a conversation marks it read (server + local count) | `MessagesPage.jsx:159-162` | **MISSING** |
46
- | 2 | Opening a conversation auto-marks read | `Thread.jsx:1581` | **MISSING** |
47
- | 3 | Global unread badge decrements by that conversation's prior count | `MessagingContext.jsx:158-169` | **MISSING** |
48
- | 4 | Opening a reply thread marks the thread read | `Thread.jsx:1464-1465` | **MISSING** |
49
- | 5 | Incoming message raises the unread count, but only for non-active conversations | `MessagingContext.jsx:68-71` | **MISSING** |
50
-
51
- Row 5 was initially assessed `OK` and corrected by the independent review — the error direction that
52
- matters, since a false `OK` leaves a real gap unfixed. The truth is worse than rows 1–4 suggested:
53
- ucm's unread state does not live-update **at all**. `applyFrame`'s `message` case
54
- (`MessagingProvider.jsx:107-110`) only reconciles `state.messages` and never touches `state.unread`;
55
- `activeConversationId`/`activeIdRef` (`MessagingProvider.jsx:139-146, 311`) is stored and exposed but
56
- never read to gate anything. The badge is populated solely by `refreshUnread()` at mount and on
57
- reconnect (`154-158, 292, 301-304`).
58
-
59
- So the unread badge today neither rises on an incoming message nor falls on reading one — it only ever
60
- reflects whatever the server said at mount. Rows 1–5 are one defect, not five nice-to-haves, and they
61
- are the first thing this WO should fix.
62
-
63
- #### Message actions
64
-
65
- | # | Capability | jg reference | Status |
66
- |---|---|---|---|
67
- | 6 | Action menu on a message (hover icon desktop / long-press mobile / right-click) | `Thread.jsx:855-894` | **MISSING** |
68
- | 7 | Edit own message — inline field, save, cancel | `Thread.jsx:912-949, 1722-1737` | **MISSING** |
69
- | 8 | Edit auto-cancels if the message is deleted elsewhere | `Thread.jsx:1618-1621` | **MISSING** |
70
- | 9 | Delete message — author **or** moderator (`delete_any`) | `Thread.jsx:1744-1768, 2010` | **MISSING** |
71
- | 10 | Delete confirmation dialog | `Thread.jsx:2438-2450` | **MISSING** |
72
- | 11 | Deleting a message being replied-to/edited clears that composer state | `Thread.jsx:1755-1761` | **MISSING** |
73
- | 12 | Copy message text | `Thread.jsx:1770-1777` | **MISSING** |
74
- | 13 | Reply to a message | `Thread.jsx:2016-2022` | OK |
75
- | 14 | Quick-reaction presets | `Thread.jsx:635-649` | DEV (5 presets vs 6) |
76
- | 15 | Full emoji picker for reactions | `Thread.jsx:650-662` | DEV |
77
- | 16 | No action menu on deleted or poll messages | `Thread.jsx:2074` | follows from 6 |
78
-
79
- Note on 7/9: `api.js` already has `patchMessage`/`deleteMessage`; neither has ever had a caller. The
80
- provider's local `patchMessage` is a same-named cache reducer — do not mistake it for the API call.
81
-
82
- #### Direct conversations
83
-
84
- | # | Capability | jg reference | Status |
85
- |---|---|---|---|
86
- | 17 | Launcher to **start** a DM | `MessagesPage.jsx:202-210` | **MISSING** |
87
- | 18 | Recipient picker over a candidate list | `NewDirectMessageDialog.jsx:22-46,74-83` | **MISSING** |
88
- | 19 | Picker loading + empty states | `NewDirectMessageDialog.jsx:71-72,82` | **MISSING** |
89
- | 20 | Start disabled until a person is chosen / while starting | `NewDirectMessageDialog.jsx:96` | **MISSING** |
90
- | 21 | Policy rejection surfaced as a readable message | `NewDirectMessageDialog.jsx:54-58,85-89` | **MISSING** |
91
- | 22 | Fullscreen dialog on small viewports | `NewDirectMessageDialog.jsx:68` | **MISSING** |
92
-
93
- `createDirectConversation` exists in `api.js` with no caller. **The candidate list is host-supplied**
94
- (dcm has no user-directory endpoint; jg feeds it from event memberships) — mirror the existing
95
- `groupLaunchers` prop pattern rather than inventing a discovery call. MSG-2b unblocked exactly this
96
- case in the backend and it currently has no client.
97
-
98
- #### Threading and quoting
99
-
100
- | # | Capability | jg reference | Status |
101
- |---|---|---|---|
102
- | 23 | Replies group under their root | `Thread.jsx:2108-2146` | **BROKEN** — server sends `reply_to_id`, `Thread.jsx:40,62` reads `reply_to`; every server-loaded reply renders as a root |
103
- | 24 | Quoted preview (sender + snippet) above a reply | `Thread.jsx:571-607` | **MISSING** |
104
- | 25 | Click the quote to jump to the original | `Thread.jsx:1997-2005` | **MISSING** |
105
- | 26 | Quote blanks when its source is deleted | `Thread.jsx:149-157` | **MISSING** |
106
- | 27 | Unread-reply dot on the thread toggle | `Thread.jsx:2128-2137` | **MISSING** |
107
- | 28 | Lazy thread fetch on expand / collapse | `Thread.jsx:1435-1486` | OK |
108
- | 29 | Per-thread reply composer | `Thread.jsx:2197-2229` | DEV |
109
-
110
- Row 23 is the highest-value single fix in this WO: it makes an already-built feature actually work.
111
-
112
- #### Composer
113
-
114
- | # | Capability | jg reference | Status |
115
- |---|---|---|---|
116
- | 30 | Shift+Enter newline, Enter sends | `Thread.jsx:1957-1962` | **PARTIAL** — verify and complete |
117
- | 31 | Staged-image preview strip with per-file remove | `Thread.jsx:507-541` | **MISSING** — only a "N attachments selected" caption today |
118
- | 32 | Upload progress indication | `Thread.jsx:2282-2288` | **MISSING** |
119
- | 33 | Client-side image compression before upload (>2 MB → max 2560 px, JPEG q0.85, silent fallback) | `Thread.jsx:304-350` | **MISSING** |
120
- | 34 | Emoji insertion into the message body at cursor | `Thread.jsx:1682-1687` | **MISSING** |
121
- | 35 | Optimistic send, retry on failure, inline error | `Thread.jsx:1856-1931` | OK |
122
- | 36 | Poll button disabled while images are staged | `Thread.jsx:2340-2347` | minor, follows from 31 |
123
- | 37 | Draft text survives a conversation switch | `Thread.jsx:1549-1582` | **do NOT reproduce** — this is a jg bug (unsent text leaks into the next conversation). Record as deliberate non-parity. |
124
-
125
- #### Conversation list
126
-
127
- | # | Capability | jg reference | Status |
128
- |---|---|---|---|
129
- | 38 | Last-message preview text | `ConversationList.jsx:155` | **BROKEN / BLOCKED** — reads `conversation.last_message?.body`; dcm never sends `last_message` |
130
- | 39 | Relative timestamp (now / m / h / d, localized) | `ConversationList.jsx:24-36` | **MISSING** |
131
- | 40 | Active-row highlight | `ConversationList.jsx:113` | **MISSING** |
132
- | 41 | Unread badge, bold row when unread | `ConversationList.jsx:121,137` | OK |
133
- | 42 | Distinguish managed *all* vs managed *team* | `ConversationList.jsx:68-71` | **decide** — dcm exposes only `kind: managed`; the distinction lives in `external_key`. Either surface it or record the loss |
134
- | 43 | Per-kind row icon | `ConversationList.jsx:39-83` | DEV (generic icons) |
135
- | 44 | Mute / archive | `Thread.jsx:2057-2061` | OK |
136
-
137
- #### Timeline
138
-
139
- | # | Capability | jg reference | Status |
140
- |---|---|---|---|
141
- | 45 | Auto-scroll to bottom on a new incoming message | `Thread.jsx:1592-1602` | **MISSING** — verify |
142
- | 46 | Announcement deep-link button on a message carrying `link_target` | `Thread.jsx:1142-1146` | **MISSING** — ucm accepts `link_target` on compose but never renders it as an action |
143
- | 47 | Sender name shown in group/managed, suppressed in 1:1 | `Thread.jsx:996-999` | **PARTIAL** — verify; currently unconditional |
144
- | 48 | Deleted tombstone, `(edited)` marker, timestamp | `Thread.jsx:823-827` | OK |
145
-
146
- #### Read receipts
147
-
148
- | # | Capability | jg reference | Status |
149
- |---|---|---|---|
150
- | 49 | Read tick on own messages | `Thread.jsx:361-400` | OK |
151
- | 50 | Per-recipient read-status detail, gated moderator **and** non-direct | `Thread.jsx:99-101, 2404-2436` | **PARTIAL** — verify tooltip vs jg's popover list; the DM carve-out must hold |
152
-
153
- #### Polls — all gated on dcm
154
-
155
- | # | Capability | jg reference | Status |
156
- |---|---|---|---|
157
- | 51 | Render question, options, per-option counts | `Thread.jsx:951-1073` | **BLOCKED** — no `serialize_poll` exists; `PollCard` reads fields no endpoint returns |
158
- | 52 | Per-option voter names | `Thread.jsx:1050-1059` | **BLOCKED** |
159
- | 53 | Closed chip, voting disabled once closed | `Thread.jsx:1014-1022` | **BLOCKED** |
160
- | 54 | Option bounds 2–10 in the composer | `Thread.jsx:80-81` | verify |
161
- | 55 | Vote / close | `Thread.jsx:1796-1834` | OK (write path works; read path blocked) |
162
-
163
- #### Live synchronisation — gated on dcm
164
-
165
- | # | Capability | jg reference | Status |
166
- |---|---|---|---|
167
- | 56 | Reactions sync live to all viewers | `Thread.jsx:1608-1610` | **BLOCKED** — dcm never emits `reaction`; ucm's handler is dead code |
168
- | 57 | Poll updates sync live | `Thread.jsx:1633-1639` | **BLOCKED** — dcm never emits `poll_updated` |
169
- | 58 | Conversation list reorders + preview updates on new/edited/deleted message | `MessagingContext.jsx:54-109` | **BLOCKED** (needs `last_message`) |
170
- | 59 | New message / edit / delete arrive live | `Thread.jsx:1604-1663` | OK — the only three frames dcm actually emits |
171
-
172
- ### Dependencies — what must land in dcm first
173
-
174
- Rows 38, 51–53, 56–58 cannot be built correctly against dcm 2.36.1. They need a dcm work order
175
- (proposed `MSG-2c`) delivering: a poll read contract (design gap — `messaging-platform.md` specifies no
176
- way to read a poll's question, options or results, so the **design must be amended first**), the
177
- missing realtime frames (`services.py` has three `_publish` call sites for twelve designed frames), and
178
- `last_message` on the conversation payload. Do not stub, fake or work around these client-side; leave
179
- them until dcm ships and say so if the sequencing slips.
180
-
181
- Everything else in this WO is buildable against dcm 2.36.1 today.
182
-
183
- ### Chunk plan — the unblocked scope (staged commits, one independent review per chunk)
184
-
185
- **In scope now: 39 rows.** Rows marked `OK` or `DEV` need no work; rows 38, 51-53 and 56-58 are
186
- BLOCKED on `MSG-2c` and stay untouched. Everything else is buildable against dcm 2.36.1 today.
187
-
188
- | Chunk | Rows | Content |
189
- |---|---|---|
190
- | 1 | 1-5, plus the contract-conformance test | Unread lifecycle end to end: mark-read on select and on open, thread read, global badge decrement, live increment for non-active conversations. Do this first — it is the one outright broken loop, and the conformance test written here guides every chunk after it. |
191
- | 2 | 23-27 | Threading: fix the `reply_to_id`/`reply_to` mismatch (row 23 makes an already-built feature work), then quoted preview, jump-to-original, quote blanking on delete, unread-reply dot. |
192
- | 3 | 6-12, 16 | Message action menu (hover / long-press / right-click), edit, delete with confirmation, copy, and the state-clearing behaviour when a message under edit or reply is deleted. |
193
- | 4 | 17-22 | DM launcher and recipient picker, candidate list host-supplied via a `groupLaunchers`-style prop. |
194
- | 5 | 30-34, 36, 37 | Composer: Shift+Enter, staged-image previews with per-file remove, upload progress, client-side image compression, emoji insertion at cursor. Row 37 is a **record-only** item — do not reproduce jg's draft leak. |
195
- | 6 | 39, 40, 42, 45-47, 50, 54, plus the corrected deviation list | List timestamp and active-row highlight, the managed all-vs-team decision, auto-scroll, announcement deep-link rendering, sender-name suppression in DMs, read-status detail, poll option bounds. Close with the deviation list rewritten against this checklist. |
196
-
197
- Rows 45, 47, 50, 54 and 42 are marked verify/decide: look at the code before calling them done in
198
- either direction, and record the outcome in the deviation list rather than leaving it implicit.
199
-
200
- **Conformance-test exemptions.** The test from chunk 1 will flag every `api.js` export without a caller.
201
- After this WO wires `markConversationRead`, `markThreadRead`, `patchMessage`, `deleteMessage` and
202
- `createDirectConversation`, three remain unwired: `createManagedConversation`,
203
- `createObjectThreadConversation` and `getMessage`. Each must either get an explicit, reasoned exemption
204
- entry or be removed — an unexplained allowlist entry reintroduces exactly the blind spot this test
205
- exists to close. The frame side of the test compares handlers against **the design's frame list**, not
206
- against what the backend currently emits, so handlers for `reaction` and `poll_updated` are correct and
207
- must not be deleted while `MSG-2c` is outstanding.
208
-
209
- ### Required tests to WRITE
210
-
211
- - **One test per row above that this WO implements.** A capability with no test is not delivered.
212
- The checklist is the coverage contract.
213
- - **Contract-conformance test (the systemic fix):** fails when a function exported from
214
- `src/messaging/api.js` has no caller in the provider or a component, and when a realtime frame type
215
- handled by the provider is not in the design's frame list (or vice versa). This test, existing
216
- earlier, would have caught rows 7, 9 and 17 (adapter functions with no caller) and rows 56–58 (dead
217
- handlers for frames the backend never emits) mechanically. It would **not** have caught row 23 — a
218
- payload field-name mismatch is a different failure shape, and needs the threading regression below.
219
- Rows 1–5 need their own coverage too: nothing about a missing capability is detectable from export
220
- wiring alone.
221
- - **Unread lifecycle:** opening a conversation clears its badge and reduces the global count by exactly
222
- that conversation's prior unread count.
223
- - **Threading regression:** replies delivered by REST **and** by realtime frame group under their root
224
- — i.e. the test must use a server-shaped payload (`reply_to_id`), not a locally-constructed one. The
225
- existing tests pass precisely because they use client-shaped objects.
226
- - **Edit/delete permissions:** author may edit own; moderator may delete any; neither affordance renders
227
- without the capability.
228
- - **DM launcher:** a first-contact DM against a candidate with no prior conversation succeeds
229
- (the MSG-2b case); a policy rejection renders a readable message.
230
- - Existing ucm suites stay green.
231
-
232
- ### Deliverables beyond code
233
-
234
- - **`docs/messaging-deviations.md` corrected.** Its "Final parity confirmation" section currently claims
235
- every jg file is represented by an exported surface or a named deviation. That is demonstrably false
236
- (edit, delete, DM launcher, mark-read, quoting, copy, image compression). Replace the claim with this
237
- WO's checklist and its per-row outcome.
238
-
239
- ### Non-goals / do-not-touch
240
-
241
- dcm changes (they belong to `MSG-2c`); host-app concerns per design §ucm surface — routing, page
242
- layout, master/detail, scope pickers, deep-link parsing; jg's own code; the jg data migration (MSG-5);
243
- search; typing; client→server WS; the decomposition achieved in MSG-3 — new capabilities land as new
244
- collaborators or in the component that owns them, **not** by growing `Thread` back toward a monolith
245
- (the ~400 LOC soft trigger and the one-harness-entry-per-component rule stay in force).
246
-
247
- ### Risks
248
-
249
- - **The checklist is long and the temptation is to batch.** Rows that get implemented without their
250
- test are how this WO reproduces MSG-3's failure at a smaller scale.
251
- - **Blocked rows invite workarounds.** Faking a poll payload client-side or synthesising a `reaction`
252
- frame would hide the dcm gap instead of closing it.
253
- - **`Thread` regrowth**, per non-goals.
254
- - Row 42 and rows 30/45/47/50/54 are marked verify/decide — they need a look at the code before being
255
- called done either way, not an assumption.
256
-
257
- ### Reviews
258
-
259
- Independent `reviewer` per chunk plus **`ui_reviewer` at WO end**. The WO-end review must check the
260
- corrected deviation list **against this checklist row by row** — the review that signed off the false
261
- completeness claim is the one this WO exists to correct.
262
-
263
- ### Preconditions
264
-
265
- dcm 2.36.1 published (met). Rows marked BLOCKED additionally require `MSG-2c`. Approval Gate #1 =
266
- operator go on this envelope.
267
-
268
- ### Release
269
-
270
- One version bump + npm publish at WO end.
271
-
272
- ### Execution directive
273
-
274
- Implement through `codex exec` in the background — invoked **directly via Bash** (never the
275
- `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check` and
276
- `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file;
277
- fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit. One
278
- invocation per chunk, each chunk left uncommitted for the orchestrator's independent review.
279
-
280
- ### Mini-handover (pastable)
281
-
282
- Orchestrator: implement `work-orders/MSG-3b.md` in `ui-core-micha` (main), chunk by chunk. `git pull`
283
- first, read the WO + `django-core-micha/docs/design/messaging-platform.md`, then follow
284
- `orchestrate-codex` (Codex-first per chunk, own review per chunk, `ui_reviewer` at WO end, one publish
285
- at WO end). Rows marked BLOCKED stay untouched until `MSG-2c` ships.
286
-
287
- ---
288
-
289
- ## Part B — Implementation map (Orchestrator)
290
-
291
- To be filled by the Orchestrator session on `git pull`, within the envelope above.
292
-
293
- **This section covers chunk 1 only (rows 1-5 + the contract-conformance test). Later chunks get
294
- their own Part B addendum before their `codex exec` invocation.**
295
-
296
- ### Chunk 1 — unread lifecycle + contract-conformance test
297
-
298
- **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root — no
299
- `backend`/`frontend` subdir in this repo).
300
-
301
- **Files to change:**
302
-
303
- - `src/messaging/MessagingProvider.jsx` — the only file with real logic changes.
304
- - `src/messaging/ConversationList.jsx` — one call site addition (row 1).
305
- - `src/messaging/Thread.jsx` — one call site addition (row 2), plus the existing `toggleReplies`
306
- function is where row 4 (thread-open marks thread read) belongs.
307
- - New test file(s) under `tests/` (see "Required tests" below) — do not touch existing test files
308
- except where a shared test helper genuinely needs extending.
309
- - `docs/messaging-deviations.md` — not touched this chunk (the doc gets rewritten once, at chunk 6,
310
- against the full checklist — do not add partial chunk-1 notes here that chunk 6 would have to
311
- reconcile).
312
-
313
- **Already in place, reuse, do not re-invent:**
314
-
315
- - `src/messaging/api.js:40-41` already exports `markThreadRead(rootId, readAt)` and
316
- `markConversationRead(conversationId, readAt)`. Neither has a caller anywhere in the codebase today
317
- — that's exactly the gap. Do not write new API functions for this; wire the existing ones.
318
- - `MessagingProvider.jsx`'s `DEFAULT_API` object (line 31-36) and the `api` param destructuring must
319
- both be extended to include `markConversationRead`/`markThreadRead`, following the exact pattern
320
- every other adapter function already uses (see `archiveConversation`/`setConversationArchived` at
321
- lines 186-189 for the closest shape: call the API, then update local state from the result).
322
- - `cache.unread` shape (from `EMPTY_CACHE` at line 30): `{ unread_count, by_conversation: { [id]: n } }`
323
- — this is also dcm's `GET unread-count/` response shape verbatim (design doc line 75).
324
-
325
- **Row-by-row implementation guidance (the "how", not literal code — Codex owns the exact shape):**
326
-
327
- - **Row 1 (select marks read) + Row 2 (open marks read):** both end up calling the same new provider
328
- action, e.g. `markConversationRead(conversationId)`, from two different call sites:
329
- `ConversationList`'s existing `onClick={() => onOpen?.(conversation)}` (around line 61) and a new
330
- `useEffect` in `Thread` keyed on `conversationId` (mirroring the existing pattern at
331
- `MessagingProvider.jsx:292`'s `useEffect(() => { if (active) refresh()... }, [active, refresh])`).
332
- Calling it from both places on the same conversation is expected and must be idempotent — the
333
- second call should not double-decrement.
334
- - **Row 3 (badge decrements by that conversation's prior count):** this is the exact bug named in the
335
- envelope's "why it's worse than rows 1-4 suggest" section — the fix must decrement
336
- `unread.unread_count` by whatever `unread.by_conversation[conversationId]` held *before* this call,
337
- not reset the badge to some assumed value and not just zero the per-conversation entry without
338
- touching the global total. Guard against going negative (`Math.max(0, ...)`, matching the existing
339
- `toggleReaction` clamp pattern at line 253).
340
- - **Row 4 (opening a reply thread marks the thread read):** wire `markThreadRead` from
341
- `Thread.jsx`'s existing `toggleReplies` function (line 48-53) — call it when a thread is being
342
- opened (not on collapse). This does not touch `cache.unread` (dcm's design has no described
343
- per-thread unread projection distinct from the parent conversation's count) — confirm against the
344
- design doc before assuming otherwise; if thread-level unread turns out to need its own state, that's
345
- a scope question to flag, not to quietly invent.
346
- - **Row 5 (incoming message raises unread, only for non-active conversations):** the real defect
347
- described in the envelope — `applyFrame`'s `message` case (line 107-110) never touches
348
- `state.unread` at all. Fix requires two things: (a) the reducer needs to know which conversation is
349
- currently active when a frame arrives — `applyFrame`/`messagingReducer` are pure and don't have
350
- access to `activeIdRef`, so the `dispatch({ type: 'frame', frame })` call in the `subscribe(...)`
351
- handler (line 293-300) must pass `activeConversationId: activeIdRef.current` alongside the frame,
352
- and `applyFrame` must accept and use it; (b) do not increment unread for a message that reconciles
353
- the current user's *own* pending optimistic send (i.e., a `reconcileMessage` match found an existing
354
- local entry by `client_request_id`) — only a genuinely new incoming message should bump the badge.
355
- `reconcileMessage` (line 62-72) already knows whether it matched; the frame handler needs that
356
- signal surfaced, not silently discarded.
357
-
358
- **Realtime frame contract note (feeds the conformance test):** the design doc (`messaging-platform.md`
359
- line 155) names twelve frame types. dcm 2.36.1 only ever emits three (`message`, `message_edited`,
360
- `message_deleted` — row 59). The provider already has handlers for `reaction`/`poll_updated` (dead
361
- code until `MSG-2c`, intentionally kept, do not delete — see the WO's conformance-test-exemptions
362
- section) plus `conversation_upsert`/`conversation_archived`/`participant_changed`. Three design frame
363
- types have **no handler at all** and are legitimately out of this chunk's scope: `attachment_ready`
364
- (reserved/deliberately unemitted per the design doc itself), `delivered`, `read_state`,
365
- `thread_read_state` (no row in this WO's checklist requires them — they're a future receipts-sync
366
- work item, not part of rows 1-5's local-count-based unread lifecycle). The conformance test's
367
- frame-side check must not fail on these three/four — give them the same kind of explicit, reasoned
368
- exemption the WO requires for the `api.js` side, not a silent allowlist.
369
-
370
- **Contract-conformance test — shape:** a Node-side static-analysis test (vitest still runs in Node;
371
- `fs`/`path` are available even under the `jsdom` test environment) that:
372
- 1. Reads `src/messaging/api.js`, extracts every `export function <name>(` — that's the full adapter
373
- surface.
374
- 2. Reads `src/messaging/MessagingProvider.jsx` and every other `src/messaging/*.jsx` component file,
375
- and checks each exported api function name appears as `api.<name>(` somewhere in that source text.
376
- 3. Fails for any export with zero call sites, **except** an explicit, named, reasoned exemption list
377
- in the test file itself: after this chunk, `createManagedConversation`, `createObjectThreadConversation`
378
- and `getMessage` remain genuinely uncalled (per the WO's "Conformance-test exemptions" section) —
379
- list them with a one-line reason each (row 42's managed/team distinction — chunk 6 — may end up
380
- calling `createManagedConversation`; if it does, remove it from the exemption list then, don't
381
- leave a stale entry).
382
- 4. Separately, reads the frame types handled in `MessagingProvider.jsx` (every `frame.type === '...'`
383
- literal) and checks each is one of the twelve types the design doc lists at
384
- `django-core-micha/docs/design/messaging-platform.md:155` — hardcode that list in the test (or read
385
- the doc file directly with `fs`, either is fine) with the same explicit-exemption treatment for the
386
- design-listed types this chunk doesn't handle (`attachment_ready`, `delivered`, `read_state`,
387
- `thread_read_state`).
388
- 5. This test is intentionally source-grepping, not behavioral — keep it in its own file
389
- (`tests/messagingContractConformance.test.jsx` or similar) so its assertions and exemption lists
390
- stay easy to find and update as later chunks close gaps.
391
-
392
- **Required tests to WRITE (chunk 1 scope):**
393
- - The contract-conformance test above.
394
- - Row 1: selecting a conversation in `ConversationList` calls `markConversationRead` for that
395
- conversation's id.
396
- - Row 2: mounting `Thread` for a conversation calls `markConversationRead` for that conversation.
397
- - Row 3: a regression test asserting the decrement is by the conversation's *prior* count specifically
398
- — seed `cache.unread` with `{ unread_count: 5, by_conversation: { 12: 2, 7: 3 } }`, mark conversation
399
- 12 read, assert `unread_count === 3` and `by_conversation[12] === 0`, `by_conversation[7]` untouched.
400
- - Row 4: expanding a reply thread calls `markThreadRead` for that root message id.
401
- - Row 5: dispatch a `message` frame for a non-active conversation and assert the badge rises by one;
402
- dispatch one for the active conversation and assert it does not; dispatch one that reconciles an
403
- existing optimistic own-message (matching `client_request_id`) and assert it does not double-count.
404
- - Existing ucm suites (139 tests) stay green.
405
-
406
- **Invariants / do-not-break:**
407
- - `applyFrame`/`messagingReducer` stay pure functions — no direct API calls or refs inside them; the
408
- active-conversation-id must be threaded in as data, not read from a ref inside the reducer.
409
- - Don't touch the `reply_to`/`reply_to_id` field-name mismatch (row 23) — that's chunk 2, out of scope
410
- here even though it's visible in `Thread.jsx` while you're in this file.
411
- - Don't touch `docs/messaging-deviations.md` this chunk.
412
- - Keep the ~400 LOC soft trigger in mind for `MessagingProvider.jsx` — it's already the largest file in
413
- the package; if this change pushes it meaningfully past that, flag it rather than silently letting it
414
- grow (splitting unread-lifecycle logic into a co-located reducer/action-creator module is an
415
- acceptable, non-monolith-regrowing way to keep it manageable, if needed).
416
-
417
- **Progress contract:** emit `PLAN: …` once, then a single-line `PROGRESS: [n/total] <action>` before
418
- every relevant action and `… done` on completion, no gap over ~2 minutes, unbuffered stdout, and a
419
- final `RESULT: DONE` or `RESULT: BLOCKED <reason>` line. Do NOT `git add`/`commit`/`push` — leave the
420
- diff for the orchestrator's review. WRITE the tests listed above and run only those to confirm they
421
- execute and pass — do not run the full suite (the orchestrator runs the affected-areas gate after).
422
-
423
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
424
- `work-orders/MSG-3b.md` chunk 1 (Part B above). Follow `orchestrate-codex`.
425
-
426
- ### Chunk 2 — threading and quoting (rows 23-27)
427
-
428
- **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root).
429
- Runs after chunk 1 is committed (its `markConversationRead`/`markThreadRead`/unread-frame plumbing
430
- already landed; this chunk does not depend on it beyond that both live in the same
431
- `MessagingProvider.jsx`).
432
-
433
- **Files to change:** `src/messaging/Thread.jsx` (root/reply grouping, unread-reply-dot state),
434
- `src/messaging/MessageBubble.jsx` (quoted preview rendering, click-to-jump, quote blanking on
435
- delete), new test coverage under `tests/`. Do not touch `docs/messaging-deviations.md` this chunk
436
- (rewritten once, at chunk 6).
437
-
438
- **Row 23 — the confirmed bug, verified against the actual dcm serializer
439
- (`django-core-micha/src/django_core_micha/messaging/serializers.py:75`):**
440
- `serialize_message` emits `"reply_to_id": str(message.reply_to_id) if ... else None` — every
441
- REST-loaded or realtime-delivered message carries `reply_to_id`, never `reply_to`. But
442
- `MessagingProvider.jsx`'s own optimistic-send builder (`sendMessage`, the `optimistic` object) sets
443
- `reply_to: payload.reply_to || null` on the **locally-constructed** row — so client-shaped optimistic
444
- messages use `reply_to` and server-shaped messages use `reply_to_id`. `Thread.jsx:40` (`!message.reply_to`
445
- for roots) and `:65` (`item.reply_to === message.id` for replies) only ever check the client-shaped
446
- name, so every server-loaded or WS-delivered reply silently renders as an unattached root — the exact
447
- defect named in the envelope.
448
-
449
- Fix by reading **both** shapes wherever a message's parent is checked (`message.reply_to_id ?? message.reply_to`),
450
- not by changing what field name the optimistic builder writes (that's `MessagingProvider.jsx`'s
451
- `sendMessage`, untouched by this chunk — rewriting it risks reopening chunk 1's just-verified
452
- optimistic-message tests for no benefit, since reading both shapes fixes the bug without it). Two call
453
- sites in `Thread.jsx` (root filter, reply filter); check `MessageBubble.jsx` and anywhere else in
454
- `src/messaging/*.jsx` for a bare `.reply_to` read that should also accept `.reply_to_id`.
455
-
456
- **Threading regression test (required, named explicitly in the WO):** must use a **server-shaped**
457
- payload — i.e. construct the test message with `reply_to_id`, not `reply_to` — for both the REST-load
458
- path (`listMessages`/`api.js` mock returning `{ ..., reply_to_id: rootId }`) and the realtime-frame
459
- path (`applyFrame`/`dispatch({ type: 'frame', ... })` with a `message` frame payload carrying
460
- `reply_to_id`). The WO is explicit that the existing tests pass "precisely because they use
461
- client-shaped objects" — do not let the new test repeat that mistake.
462
-
463
- **Row 24 (quoted preview) + Row 25 (click-to-jump) + Row 26 (quote blanks when its source is
464
- deleted):** `Thread.jsx:66,68` already passes the full root message object as `MessageBubble`'s
465
- `replyTo` prop when rendering a reply row — so `MessageBubble` already receives everything it needs
466
- (sender, body, deleted_at) without a new fetch. Extend the existing `{replyTo && <Typography ...>}`
467
- block (`MessageBubble.jsx:19`) from a sender-only caption into an actual quoted preview: sender name
468
- plus a short snippet of `replyTo.body`/`replyTo.title` (truncate client-side, no new i18n key needed
469
- beyond what's already there unless you introduce a distinct "quote" string — reuse
470
- `MessagingThread.REPLY_TO` if its copy still fits, otherwise add one translation key in all three
471
- locales, following the existing `messagingTranslations.ts` shape). Row 26: if `replyTo.deleted_at` is
472
- set, render a blanked/deleted placeholder in the quote instead of `replyTo.body` (soft-deleted messages
473
- already have `body`/`title` nulled server-side per the design doc — the placeholder text is what needs
474
- adding, the null-safety is already structurally there). Row 25: make the quote block clickable
475
- (`onClick`/`role="button"` or a `Button`, matching `MessageBubble`'s existing MUI-only convention —
476
- see the ui_reviewer's earlier MSG-3 finding on raw `<button>` usage, do not repeat it) and scroll the
477
- original into view — `Thread.jsx` owns the scroll container (`scrollRef`, line 34); the click handler
478
- needs a way to reach a specific message's DOM node from `MessageBubble`, e.g. an `id`/`data-message-id`
479
- attribute on each bubble's root `Paper` plus a callback prop threaded down from `Thread`
480
- (`onJumpToMessage`) that does `scrollRef.current.querySelector(...)?.scrollIntoView(...)`. Keep this
481
- addition in `Thread`/`MessageBubble` only — no new top-level exported component needed, this is
482
- within an existing component's remit, not new decomposition surface.
483
-
484
- **Row 27 (unread-reply dot on the thread toggle) — verify data availability before implementing,
485
- this needs the same care as the WO's explicit verify/decide rows even though it isn't tagged one:**
486
- investigated against dcm — `MessageThreadReceipt` (`root,user,last_read_at`) exists as a model
487
- (design doc line 25) and is written by `POST messages/{root_id}/thread/read/`
488
- (`django-core-micha/src/django_core_micha/messaging/services.py:286-289`), but **no GET endpoint or
489
- message-serializer field currently exposes a thread's `last_read_at` or an unread-reply flag/count to
490
- the client** — `serialize_message` (`serializers.py:60-79`) has no such field, and there is no
491
- "list threads with receipt state" endpoint. A true, cross-device-accurate unread-reply dot is
492
- therefore not buildable against dcm 2.36.1 — this is a real (if narrower and previously-unnoticed) gap
493
- of the same shape as the checklist's explicitly-BLOCKED rows, discovered while implementing, not
494
- assumed in advance.
495
-
496
- Do not silently drop it and do not fake server data. Two acceptable outcomes, pick one and record it
497
- explicitly in this chunk's summary (for chunk 6's deviation-doc rewrite to pick up verbatim):
498
- (a) build a **session-local, client-only heuristic** — track locally (e.g. a ref/map keyed by root id)
499
- the last time this provider instance called `markThreadRead` for that root in the current session, and
500
- show the dot when a root has replies (`reply_count`/loaded replies) newer than that local marker or
501
- never marked in this session; test it as exactly that — a session-local approximation, not a synced
502
- read receipt — and the deviation-doc entry must say so plainly (no cross-device/cross-tab accuracy
503
- until dcm exposes thread receipt state), or (b) skip implementation and record it as a straight
504
- `BLOCKED`-shaped deviation alongside rows 38/51-53/56-58, with the one-line reason above. Do not spend
505
- this chunk inventing a third option (e.g. guessing at an undocumented endpoint) — the two above are
506
- the only ones consistent with "do not stub, fake or work around blocked capabilities."
507
-
508
- **Required tests to WRITE (chunk 2 scope):**
509
- - The threading regression test described above (REST + realtime, both `reply_to_id`-shaped).
510
- - Quoted preview renders sender + snippet for a reply; blanks correctly when the quoted source is
511
- deleted.
512
- - Click-to-jump scrolls/focuses the original message (test what's observable in jsdom — e.g. that the
513
- jump handler is invoked with the right message id / that `scrollIntoView` is called on the right
514
- node — not literal pixel scroll position).
515
- - Row 27's outcome, whichever of (a)/(b) above: if (a), a test proving the session-local dot
516
- appears/clears correctly; if (b), no test needed but the decision must still be stated in the
517
- chunk's own summary text.
518
- - Existing ucm suites (chunk-1-inclusive baseline) stay green.
519
-
520
- **Invariants / do-not-break:** don't touch chunk 1's unread-lifecycle code paths; don't touch
521
- `docs/messaging-deviations.md`; keep the fix to reading both `reply_to`/`reply_to_id` shapes rather
522
- than changing what the optimistic builder writes; no new top-level `src/index.js` export needed for
523
- this chunk (quoting/jump/dot are internal `Thread`/`MessageBubble` behavior, not new mountable
524
- surfaces) — if you find yourself wanting one, stop and flag it rather than assuming it's warranted.
525
-
526
- **Progress contract / preamble:** identical to chunk 1's (see above) — `PLAN:`/`PROGRESS:`/`RESULT:`
527
- lines, no git operations, write-and-run-only-the-new-tests, leave the diff uncommitted.
528
-
529
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
530
- `work-orders/MSG-3b.md` chunk 2 (Part B above, after chunk 1 is committed). Follow `orchestrate-codex`.
531
-
532
- **Chunk 2 outcome record — row 27 (unread-reply dot), for chunk 6's deviation-doc rewrite to copy
533
- verbatim:** **BLOCKED, not implemented.** Verified against dcm 2.36.1
534
- (`django-core-micha/src/django_core_micha/messaging/views.py:302-307`'s `ThreadReadView.post` only
535
- returns the actor's own just-written `last_read_at` from marking a thread read — there is no GET
536
- endpoint or `serialize_message`/`serialize_conversation` field (`serializers.py:60-79`) exposing a
537
- thread's prior receipt state to the client). No truthful unread-reply dot can be rendered — client or
538
- server — without dcm exposing thread receipt state to read, not just write. This is the same shape of
539
- gap as the checklist's already-BLOCKED rows (38, 51-53, 56-58), discovered during chunk 2 rather than
540
- scoped in advance; row 27 should be treated as `BLOCKED` alongside them for any future `MSG-2c`-style
541
- dcm work, not attempted client-side.
542
-
543
- ### Chunk 3 — message actions: menu, edit, delete, copy (rows 6-12, 16)
544
-
545
- **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root). Runs
546
- after chunk 2 is committed.
547
-
548
- **Files to change:** `src/messaging/MessageBubble.jsx` (action menu, edit-inline UI, delete
549
- confirmation), `src/messaging/MessagingProvider.jsx` (wire the already-existing, never-called
550
- `patchMessage`/`deleteMessage` `api.js` exports — note the provider already has a same-named **local**
551
- reducer action-creator called `patchMessage` at `MessagingProvider.jsx:247`, which is a cache merge
552
- helper, not an API call; do not confuse the two or silently overload the name — expose the new API
553
- call under a distinct name, e.g. `editMessage`/`removeMessage`, and keep the existing local
554
- `patchMessage` cache helper as-is, used internally the way it already is), `src/messaging/Thread.jsx`
555
- or `Composer.jsx` (row 11 — clearing reply/edit state when the message under edit or reply is
556
- deleted), `src/i18n/messagingTranslations.ts` (new copy — menu labels, edit save/cancel, delete
557
- confirm dialog, copy), new tests under `tests/`.
558
-
559
- **Already in place, reuse:** `src/messaging/api.js:35-36` exports `patchMessage(messageId, patch)` and
560
- `deleteMessage(messageId)`, both unwired (the two of chunk 1's three originally-named conformance-test
561
- exemptions this chunk closes — remove `patchMessage`/`deleteMessage` from
562
- `tests/messagingContractConformance.test.js`'s `API_EXEMPTIONS` once wired, per the exemption note
563
- already left there). dcm's actual permission logic (verified against
564
- `django-core-micha/src/django_core_micha/messaging/services.py:167-192`): `edit_message` allows the
565
- sender or anyone with `edit_any` in `moderation_rights(...)`; `soft_delete_message` the same with
566
- `delete_any`; editing a deleted message raises; deletion is idempotent (already-deleted is a no-op,
567
- not an error). The PATCH/DELETE endpoints are `django-core-micha`'s
568
- `MessageDetailView` (`views.py:227-235`) — `PATCH messages/{id}/` (`body`/`title`/`link_target` only,
569
- per the design doc's REST table) and `DELETE messages/{id}/`.
570
-
571
- **A real gap found while scoping this chunk, needs a decision, not silent assumption:** `serialize_message`
572
- (`django-core-micha/src/django_core_micha/messaging/serializers.py:60-79`) never includes any
573
- per-viewer capability field (no `can_edit`/`can_delete`/`is_moderator`) — moderator status
574
- (`edit_any`/`delete_any`) is resolved server-side from each app's `MessagingPolicy` hook and never
575
- surfaced to the client in any payload this repo has access to. That matches the design doc's stated
576
- model (`messaging-platform.md`: "Event-manager rights map to moderation/config capabities" — i.e.
577
- moderator-ness is host-app domain knowledge, not a generic messaging-REST fact), and mirrors the
578
- existing `allowAnnouncement`/`linkTarget` pattern in `Composer.jsx` (a host-supplied capability prop,
579
- since the client can't compute it itself).
580
-
581
- Implement accordingly: the action menu/edit/delete affordances always render for the message's own
582
- sender (`message.sender?.id === currentUser?.id`, matching the existing `canShowReadTicks` pattern in
583
- `Thread.jsx`) **and** additionally for anyone when a new host-supplied capability prop indicates
584
- moderator rights — thread this down from wherever it naturally enters (e.g. a `canModerateMessages`
585
- boolean prop on `Thread`, passed through to `MessageBubble`, defaulting to `false`). The server remains
586
- the actual enforcement (a 403 from a stale/wrong host-side flag must surface as a readable error, not
587
- crash) — client-side gating is UX-only, not the security boundary. Do not invent a REST call to fetch
588
- moderator status that doesn't exist; do not skip the moderator case entirely either (that would silently
589
- drop row 9's "or moderator" half without recording it) — the host-prop approach satisfies both.
590
-
591
- **Row 6 (action menu):** MUI `Menu`/`IconButton` triggered by a hover-revealed icon (desktop) — jg
592
- additionally does long-press (mobile) and right-click; matching the WO's general redesign licence
593
- (interaction may be redesigned, capability may not be lost), a plain always-visible-on-hover icon plus
594
- a right-click (`onContextMenu`) handler opening the same menu is sufficient parity; long-press-to-open
595
- context menu on touch is a reasonable **explicit deviation** to record (desktop hover + right-click
596
- covers the capability; a dedicated long-press gesture handler is disproportionate for a MUI-only
597
- component) rather than something to build a custom touch-gesture library for.
598
-
599
- **Row 7 (edit) + Row 8 (edit auto-cancels if deleted elsewhere):** inline edit swaps the message body
600
- for a `TextField` with save/cancel, calling the new `editMessage` provider action; row 8 needs the edit
601
- state to react to a live `message_deleted` frame or `messagePatched`/cache update marking the message
602
- deleted while it's being edited — since edit state is local `useState` in `MessageBubble` (or wherever
603
- you put it), watch `message.deleted_at` becoming true (already reactive via props, since
604
- `MessageBubble` re-renders on cache changes) and close the edit UI when it does, rather than polling.
605
-
606
- **Row 9 (delete, author or moderator) + Row 10 (confirmation dialog) + Row 11 (deleting a
607
- message under edit/reply clears that composer state):** a `Dialog` confirm before calling the new
608
- `removeMessage` provider action. Row 11: `Composer.jsx`'s `replyTarget` prop and `Thread.jsx`'s
609
- `replyingTo`/edit-in-progress state need to clear if the specific message they reference becomes
610
- deleted — same reactive-to-`deleted_at` approach as row 8, surfaced via the existing
611
- `onReplyTargetChange` callback `Composer` already accepts.
612
-
613
- **Row 12 (copy message text):** `navigator.clipboard.writeText(message.body)` from the action menu; no
614
- special test-environment handling needed beyond mocking `navigator.clipboard` in the test (a common,
615
- already-solved jsdom pattern — check whether any existing test in this repo already mocks clipboard
616
- before adding a new mocking approach).
617
-
618
- **Row 16 (no action menu on deleted or poll messages):** the menu trigger must not render when
619
- `message.deleted_at` is set (already implied — nothing to edit/delete/copy on a tombstone) or when
620
- `message.kind === 'poll'` (poll messages have their own `PollCard` mutation surface; the WO frames this
621
- as "follows from 6," i.e. a one-line gate on the menu-render condition, not new logic).
622
-
623
- **Required tests to WRITE (chunk 3 scope):**
624
- - Action menu renders for the sender; renders for a non-sender only when the moderator capability
625
- prop is true; does not render for a non-sender/non-moderator; does not render on a deleted or poll
626
- message.
627
- - Edit: author can edit own message (calls the API with the right payload); edit auto-closes when the
628
- message's `deleted_at` flips true mid-edit.
629
- - Delete: confirmation dialog gates the actual delete call; author-delete and moderator-delete (via the
630
- capability prop) both succeed; a non-author/non-moderator never gets the affordance in the first
631
- place (so there's nothing to test-block client-side beyond "the button isn't there" — don't invent a
632
- client-side permission-check unit test for a check the server owns).
633
- - Row 11: deleting a message currently targeted by `replyTarget`/edit-in-progress clears that state.
634
- - Copy: clipboard write is called with the message's exact body text.
635
- - Existing ucm suites (chunks 1-2 inclusive) stay green.
636
-
637
- **Deliverable note for chunk 6's deviation-doc rewrite:** record two things explicitly — the
638
- long-press-vs-hover/right-click interaction deviation (row 6), and the moderator-capability
639
- host-supplied-prop mechanism (rows 7/9), including that dcm exposes no server-side capability field so
640
- this is inherently a UX-only gate backed by server enforcement, not a security boundary claim.
641
-
642
- **Invariants / do-not-break:** don't touch chunks 1-2's code paths; don't rename/repurpose the existing
643
- local `patchMessage` cache-merge action creator in `MessagingProvider.jsx` — add new, distinctly-named
644
- functions for the API calls; keep `docs/messaging-deviations.md` untouched this chunk; keep the
645
- ~400 LOC soft trigger in mind for `MessageBubble.jsx` (already grew in chunk 2) — if the menu/edit/
646
- delete UI pushes it over, consider a small `MessageActions`-style co-located subcomponent rather than
647
- letting one file sprawl, but this does not need to become a new top-level exported/index.js surface
648
- unless it's independently useful outside a bubble (it isn't here).
649
-
650
- **Progress contract / preamble:** identical to chunks 1-2's — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
651
- git operations, write-and-run-only-the-new-tests, leave the diff uncommitted.
652
-
653
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
654
- `work-orders/MSG-3b.md` chunk 3 (Part B above, after chunk 2 is committed). Follow `orchestrate-codex`.
655
-
656
- ### Chunk 4 — DM launcher and recipient picker (rows 17-22)
657
-
658
- **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root). Runs
659
- after chunk 3 is committed.
660
-
661
- **Files to change:** a new `src/messaging/DirectMessageLauncher.jsx` (or similar name — a new,
662
- independently-exported, independently-mountable component per the WO's decomposition rule, mirroring
663
- `ConversationLaunchers.jsx`'s existing shape rather than folding this into `ConversationList.jsx`),
664
- `src/messaging/MessagingProvider.jsx` (wire the already-existing, never-called
665
- `createDirectConversation` export), `src/index.js` (new export), `dev/entries.jsx` (new harness
666
- entry — binding requirement, not optional), `src/i18n/messagingTranslations.ts`, new tests.
667
-
668
- **Already in place, reuse:** `src/messaging/api.js:21` exports `createDirectConversation(payload)` with
669
- zero callers — the exact chunk-1-named gap. Verified against dcm's actual view
670
- (`django-core-micha/src/django_core_micha/messaging/views.py:131-141`, `DirectConversationView.post`):
671
- payload is `{ target_user_id, scope? }` (`scope` optional — omit for the global scope, matching
672
- `open_direct`'s default in `services.py:79-83`). A policy rejection (e.g. `dm_policy` disallows this
673
- pair) surfaces as a plain DRF `PermissionDenied` → generic `{"detail": "..."}` 403
674
- (`views.py:112-113`) — `extractApiErrorMessage` (already in `MessagingProvider.jsx`) already handles
675
- this shape; no new error-parsing logic needed, just a place to render it.
676
-
677
- **The candidate list is host-supplied — do not invent a discovery call.** dcm has no user-directory/
678
- membership-listing endpoint (verified: no such view exists in `messaging/views.py`); jg feeds its
679
- picker from event memberships, which is app-specific domain data outside messaging's remit. Mirror the
680
- existing `groupLaunchers` prop pattern (`ConversationLaunchers.jsx:12`, an array of
681
- `{id, label, payload}` the host constructs from its own data) — this component takes a
682
- `candidates` prop (e.g. `[{id, display_name}, ...]`) the host supplies, plus a `scope` value to pass
683
- through to `createDirectConversation`. Do not add a REST call, a search box hitting a nonexistent
684
- endpoint, or any client-side "list users" logic.
685
-
686
- **Row 17 (launcher to start a DM):** a button/affordance opening the picker dialog — same placement
687
- convention as `ConversationLaunchers`' existing broadcast/group buttons (this new component can be
688
- rendered alongside `ConversationLaunchers` by a host, or the host may choose to compose them together
689
- in its own layout — do not merge the two components into one, they have independent host-supplied
690
- data shapes and the WO's decomposition rule favors small, single-purpose collaborators).
691
-
692
- **Row 18 (recipient picker over the candidate list) + Row 19 (loading + empty states) + Row 20 (start
693
- disabled until chosen / while starting):** a `Dialog` with a list/autocomplete over the `candidates`
694
- prop (a plain MUI `List`/`ListItemButton` selection is sufficient — an `Autocomplete` is also fine if
695
- it fits better, either is within the existing MUI-only convention); "loading" here means the dialog's
696
- own in-flight `createDirectConversation` call (there's no separate candidate-fetch to show a loading
697
- state for, since candidates arrive as a prop already resolved by the host — don't invent a loading
698
- state for data that isn't being fetched by this component); "empty" is an empty `candidates` array.
699
- The start/confirm button is disabled with nothing selected and while the create call is in flight
700
- (mirror `Composer.jsx`'s `sending`-gated `disabled` pattern).
701
-
702
- **Row 21 (policy rejection surfaced as a readable message):** on a `createDirectConversation` rejection,
703
- show `extractApiErrorMessage(error)` in an `Alert` inside the dialog (matching every other
704
- messaging-component error pattern in this repo — `ConversationList.jsx`, `Composer.jsx`, etc.), not a
705
- silent failure or a generic toast.
706
-
707
- **Row 22 (fullscreen dialog on small viewports):** MUI `Dialog`'s `fullScreen` prop driven by
708
- `useMediaQuery(theme.breakpoints.down('sm'))` — this repo has essentially no existing
709
- `useMediaQuery`/breakpoint usage (noted as a residual risk in the MSG-3 `ui_reviewer` pass, not a
710
- blocker), so this is a legitimate, WO-required first use, not scope creep; keep it scoped to this one
711
- dialog, don't retrofit other components.
712
-
713
- **MSG-2b connection (context, not new work):** the WO's envelope notes MSG-2b already unblocked the
714
- backend first-contact-DM case; this chunk is purely the client wiring — no dcm-side work, no
715
- verification of MSG-2b itself needed beyond the payload shape already confirmed above.
716
-
717
- **Required tests to WRITE (chunk 4 scope):**
718
- - The DM launcher opens the picker; picker renders the host-supplied candidate list; empty-candidates
719
- state renders distinctly.
720
- - Start is disabled with no selection and while the create call is pending; enabled once a candidate is
721
- chosen.
722
- - Selecting a candidate and confirming calls `createDirectConversation` with the right
723
- `target_user_id`/`scope` and opens the resulting conversation (mirror `ConversationLaunchers`'
724
- existing `onOpen` callback convention).
725
- - A policy-rejection response renders a readable error in the dialog, does not close it, and does not
726
- crash.
727
- - `fullScreen` responds to the small-viewport media query (test via mocking `useMediaQuery` or
728
- `window.matchMedia`, whichever this repo's existing test setup already supports — check
729
- `tests/setup`/`vitest.config` for an existing `matchMedia` mock before adding a new one).
730
- - Existing ucm suites (chunks 1-3 inclusive) stay green.
731
-
732
- **Deliverable note for chunk 6:** this chunk closes the third of chunk 1's three originally-named
733
- conformance-test exemptions (`createDirectConversation`) — remove it from
734
- `tests/messagingContractConformance.test.js`'s `API_EXEMPTIONS` once wired (mirror how chunk 3 already
735
- removed `patchMessage`/`deleteMessage`).
736
-
737
- **Invariants / do-not-break:** don't touch chunks 1-3's code paths; keep
738
- `docs/messaging-deviations.md` untouched this chunk (chunk 6 rewrites it once); this is new
739
- decomposition surface, so it DOES need a `src/index.js` export and a `dev/entries.jsx` harness entry
740
- (unlike chunks 2-3's internal-only changes) — don't skip either.
741
-
742
- **Progress contract / preamble:** identical to chunks 1-3's — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
743
- git operations, write-and-run-only-the-new-tests, leave the diff uncommitted.
744
-
745
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
746
- `work-orders/MSG-3b.md` chunk 4 (Part B above, after chunk 3 is committed). Follow `orchestrate-codex`.
747
-
748
- ### Chunk 5 — composer: shortcuts, previews, progress, compression, emoji (rows 30-34, 36, 37)
749
-
750
- **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root). Runs
751
- after chunk 4 is committed.
752
-
753
- **Files to change:** `src/messaging/Composer.jsx` (all six rows land here — it's the composer, not new
754
- decomposition surface, so no new `src/index.js` export or harness entry needed, just editing the
755
- existing mounted component), `src/i18n/messagingTranslations.ts`, new tests. `docs/messaging-deviations.md`
756
- untouched this chunk.
757
-
758
- **Row 30 (Shift+Enter newline, Enter sends — currently PARTIAL, verify and complete):** the `TextField`
759
- at `Composer.jsx:94` is a bare `multiline` field with no `onKeyDown` handler at all — Enter currently
760
- just inserts a newline (native textarea behavior), it does not submit. Add an `onKeyDown` that submits
761
- on plain `Enter` (calling the existing `submit()` function) and allows the default newline behavior on
762
- `Shift+Enter` (do not `preventDefault` in that branch). Guard against submitting on IME composition
763
- (`event.nativeEvent.isComposing`) if you want to be thorough, but the WO's minimum bar is the
764
- Enter-sends/Shift+Enter-newline split itself.
765
-
766
- **Row 31 (staged-image preview strip, per-file remove — currently only a "N attachments selected"
767
- caption):** replace/extend the `files.length > 0` caption at `Composer.jsx:88` with a horizontal strip
768
- of thumbnails for image files (`file.type.startsWith('image/')` — use `URL.createObjectURL(file)`,
769
- matching the existing revoke-on-cleanup pattern already used in `AttachmentList.jsx:15-25` for
770
- downloaded thumbnails, don't leak object URLs) plus a filename chip for non-image files, each with a
771
- small remove (×) control that splices that one file out of the `files` state array. Keep the existing
772
- "N attachments selected" caption as a fallback/count label if useful, or replace it entirely with the
773
- strip — either is fine, the capability (see what's staged, remove one) is what's required.
774
-
775
- **Row 32 (upload progress indication):** `sendAttachments` (`MessagingProvider.jsx`) calls
776
- `api.uploadAttachments(conversationId, formData, {...})`, which goes through `apiClient` (axios) —
777
- axios requests support an `onUploadProgress` callback. Check `src/messaging/api.js:32`
778
- (`uploadAttachments`) and `src/auth/apiClient.jsx` for how request config is threaded through, and
779
- whether adding an `onUploadProgress` option is a small, additive change to `uploadAttachments`'s
780
- signature/config (it should be — axios accepts it in the request config object) versus something that
781
- needs a larger plumbing change. If threading real byte-progress through turns out to require touching
782
- several layers disproportionately, an indeterminate `CircularProgress`/`LinearProgress` shown for the
783
- duration of the upload (already-existing `sending` state) is an acceptable minimum — but attempt real
784
- progress first since the WO names it explicitly and the axios hook is a small addition, not a redesign.
785
-
786
- **Row 33 (client-side image compression, >2MB → max 2560px, JPEG q0.85, silent fallback):** verified
787
- against dcm — the server's own limit is 25MB (`django-core-micha/src/django_core_micha/messaging/attachments.py:23`,
788
- `MAX_ATTACHMENT_SIZE`), so this compression is purely a client-side bandwidth/upload-time optimization,
789
- not required for the server to accept anything; a compression failure must silently fall back to the
790
- original file (never block or corrupt the send) exactly as the WO's row text specifies. Implement via
791
- canvas: draw the image onto a `<canvas>` scaled down so its longest edge is ≤2560px (skip entirely if
792
- already smaller), then `canvas.toBlob(callback, 'image/jpeg', 0.85)`, wrap in a promise, and only
793
- apply this to files over 2MB (`file.size > 2 * 1024 * 1024`) and of an image MIME type — non-image
794
- files and images ≤2MB pass through unchanged. Wrap the whole compression attempt in try/catch (or a
795
- promise `.catch`) that resolves to the original `File` on any failure (canvas/image-decode errors,
796
- unsupported format, etc.) — this is the "silent fallback" the WO requires, not optional robustness.
797
-
798
- **Row 34 (emoji insertion into the message body at cursor):** an emoji picker button next to the
799
- existing attachment/poll/announcement `IconButton`s in `Composer.jsx:89-93`, inserting the picked
800
- emoji into `body` at the current cursor position (track/read `TextField`'s underlying `<textarea>`
801
- `selectionStart`/`selectionEnd` via a ref, splice the emoji in, then restore focus/cursor position
802
- after the emoji). Row 15 (`ReactionBar.jsx:8`, `QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '👀']`) already
803
- established this package's precedent of a small fixed emoji set rather than a full picker library
804
- (recorded as an accepted `DEV` deviation from jg's `EmojiPickerButton.jsx` in the existing deviation
805
- doc) — reuse that same small-set convention here for consistency rather than introducing a different
806
- emoji selection UI or a new picker dependency; a `Menu`/`Popover` of the same or a similarly-sized
807
- curated emoji set opened from the new button is sufficient. Don't add a new npm dependency for this.
808
-
809
- **Row 36 (poll button disabled while images are staged) — trivial, follows from row 31:** once row 31's
810
- staged-file state exists, add `disabled={pollOpen indicator || files.some(f => f.type.startsWith('image/'))}`-style
811
- gating to the existing poll `IconButton` (`Composer.jsx:92`) — a one-line condition addition, not new
812
- logic; verify against the WO's exact framing (jg disables poll-composing while images are staged,
813
- presumably because a message can't carry both a poll and attachments in one send) before wiring it,
814
- i.e. confirm this is really about mutual exclusivity of poll vs. attachment in a single send (check
815
- `send_message`/`create_conversation` service logic in dcm if the reasoning isn't obvious from the
816
- existing chat-send code path) rather than assume the reason without checking.
817
-
818
- **Row 37 (draft text does NOT survive a conversation switch — explicit non-goal, jg bug, do NOT
819
- reproduce):** confirm current behavior already satisfies this (a fresh `Composer` mount per
820
- `conversationId` via React's key/prop change naturally resets local `useState` — check whether the
821
- host's typical usage pattern, e.g. a `key={conversationId}` on `Composer` or equivalent remount, is
822
- already implied elsewhere in this codebase, or whether `Composer`'s `body`/`files` state could
823
- persist across a `conversationId` prop change without a remount). If `Composer` is typically kept
824
- mounted while `conversationId` changes (rather than remounted), add an effect resetting `body`/`files`
825
- when `conversationId` changes, so unsent text never leaks into the next conversation — this is a
826
- one-line fix if needed, not a redesign. No test is required if you confirm the existing remount pattern
827
- already prevents this (document which is true in your chunk summary), but if you add the reset effect,
828
- test it.
829
-
830
- **Required tests to WRITE (chunk 5 scope):**
831
- - Row 30: Enter submits (with body present); Shift+Enter inserts a newline without submitting.
832
- - Row 31: staged images render a preview strip; removing one via its control drops it from the
833
- eventual submitted `FormData`/`files[]` without affecting the others.
834
- - Row 32: an in-flight upload shows progress (whichever form — byte-percentage or indeterminate,
835
- matching whichever you implemented); test what's actually observable (e.g. an `onUploadProgress`
836
- callback firing and updating visible state, or the indeterminate indicator rendering during
837
- `sending`).
838
- - Row 33: a file over 2MB is compressed (mock canvas/`toBlob` — check whether this repo's existing test
839
- setup already provides a canvas/image mock before adding a new one) before being included in the
840
- upload's `FormData`; a compression failure still results in the original file being sent, not a
841
- blocked/broken send.
842
- - Row 34: picking an emoji inserts it into `body` at the cursor position (not just appended to the
843
- end — construct a test with a non-trivial cursor position to actually prove insertion-at-cursor, not
844
- insertion-at-end, which would pass a weaker test trivially).
845
- - Row 36: poll button is disabled once an image is staged, enabled again once removed.
846
- - Row 37: either a passing test proving draft state doesn't leak across a `conversationId` change, or
847
- (if you conclude the existing remount pattern already prevents it) no new test, but state that
848
- conclusion explicitly in your chunk summary.
849
- - Existing ucm suites (chunks 1-4 inclusive) stay green.
850
-
851
- **Invariants / do-not-break:** don't touch chunks 1-4's code paths; keep `Composer.jsx` component
852
- cohesive — if this chunk pushes it meaningfully past the ~400 LOC soft trigger, a small co-located
853
- helper module (e.g. `composerImageCompression.js` for the canvas logic, kept internal/unexported) is an
854
- acceptable way to manage size without violating the no-new-top-level-surface rule (it's not a new
855
- mountable component, just an extracted pure function); flag rather than silently letting one file
856
- sprawl if it happens anyway. No new npm dependency for the emoji picker (row 34) or anything else in
857
- this chunk — everything here is buildable with existing MUI + native browser APIs (canvas, axios
858
- progress hooks).
859
-
860
- **Progress contract / preamble:** identical to chunks 1-4's — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
861
- git operations, write-and-run-only-the-new-tests, leave the diff uncommitted.
862
-
863
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
864
- `work-orders/MSG-3b.md` chunk 5 (Part B above, after chunk 4 is committed). Follow `orchestrate-codex`.
865
-
866
- ### Chunk 6 — list polish, timeline polish, receipts, poll bounds, and the deviation-doc rewrite (rows 39, 40, 42, 45-47, 50, 54)
867
-
868
- **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root). Runs
869
- after chunk 5 is committed. This is the WO's last content chunk — after it lands, the WO-end
870
- `ui_reviewer` pass and the version-bump/publish are the only remaining steps.
871
-
872
- **Files to change:** `src/messaging/ConversationList.jsx` (rows 39, 40, 42), `src/messaging/Thread.jsx`
873
- + `src/messaging/MessageBubble.jsx` (rows 45, 46, 47), `src/messaging/ReadTicks.jsx` (row 50),
874
- `src/messaging/Composer.jsx` (row 54), `src/i18n/messagingTranslations.ts`, new tests, and finally
875
- **`docs/messaging-deviations.md` — rewritten once, at the end of this chunk**, against the WO's full
876
- checklist (this is the deliverable the WO's envelope calls out as correcting MSG-3's false completeness
877
- claim).
878
-
879
- **Row 39 (relative timestamp, localized) + Row 40 (active-row highlight):** `ConversationList.jsx`
880
- currently renders no timestamp at all (`ListItemText`'s `secondary` is the last-message preview only).
881
- Add a relative-time label ("now"/"5m"/"3h"/"2d") next to or under the title, computed from
882
- `conversation.last_message_at`, using the browser-native `Intl.RelativeTimeFormat` (locale-aware,
883
- zero new dependency — this repo has no existing relative-time helper to reuse, don't add a date
884
- library for this). For row 40: `ListItemButton` already sets `selected={conversation.id ===
885
- activeConversationId}` (`ConversationList.jsx:61`) — MUI's `selected` prop does apply a background
886
- highlight by default, so verify in the rendered/test DOM whether this is actually visually
887
- distinguishable (check the applied `Mui-selected` class is present) before assuming it needs new work;
888
- if the existing `selected` wiring already produces a real highlight, this row may already be `OK` and
889
- should be recorded as such rather than reimplemented — don't add redundant styling on top of something
890
- that already works, verify first.
891
-
892
- **Row 42 (managed *all* vs. managed *team* — decide, not build):** verified against dcm — the
893
- `Conversation` model has `external_key` (`django-core-micha/src/django_core_micha/messaging/models.py:86`),
894
- which is where jg's all-vs-team distinction lives, but `serialize_conversation_core`
895
- (`serializers.py:106-113`) never includes `external_key` in any REST response. This field genuinely
896
- cannot be surfaced client-side without a dcm change (out of this WO's scope — dcm changes belong to a
897
- future `MSG-2c`-style order). **Decision: record as a deviation, do not attempt to guess the
898
- distinction from `title` text or any other client-visible field.** This is a `decide` row, and the
899
- decision is: not buildable against dcm 2.36.1, closest in shape to the already-BLOCKED rows.
900
-
901
- **Row 45 (auto-scroll to bottom on a new incoming message — verify, currently missing):** `Thread.jsx`'s
902
- scroll container (`scrollRef`, line 34) has no effect reacting to new messages arriving in `roots`. Add
903
- one: on a new message appended to the active conversation, scroll to bottom — but gate it on the user
904
- already being near the bottom before the new message arrived (e.g. `scrollHeight - scrollTop -
905
- clientHeight` under some small threshold), so a user scrolled up reading history doesn't get yanked to
906
- the bottom by an unrelated new message. This nuance isn't explicit in the WO's one-line row text but is
907
- standard chat-UI behavior and avoids a real usability regression — implement it, and note the
908
- near-bottom gating as a considered addition in your chunk summary (not silently assumed).
909
-
910
- **Row 46 (announcement deep-link button, `link_target` accepted on compose but never rendered as an
911
- action):** `MessageBubble.jsx` has no rendering for `message.link_target` at all currently. Add a
912
- button/link shown when `!deleted && message.kind === 'announcement' && message.link_target` — but the
913
- link's *destination handling* is host-app routing (same reasoning already established for
914
- `resolveLink` on `NotificationBell` elsewhere in this package), so this needs a host-supplied callback
915
- prop threaded `Thread` → `MessageBubble` (e.g. `onAnnouncementLink(linkTarget)`), not a hardcoded
916
- `window.location`/`<a href>` navigation. Without a host callback supplied, render nothing extra (no
917
- broken/no-op link) rather than a non-functional button.
918
-
919
- **Row 47 (sender name shown in group/managed, suppressed in 1:1 — PARTIAL, currently unconditional):**
920
- `MessageBubble.jsx:31` (`senderName(message)`) renders unconditionally. `Thread.jsx` already has
921
- `conversation` in scope (line 40) but doesn't pass it to `MessageBubble` — thread it through (a new
922
- `conversation` prop on `MessageBubble`, alongside the existing `replyTo`/`onReply`/etc.) and suppress
923
- the sender-name line when `conversation?.kind === 'direct'` (matches jg's 1:1 convention: in a two-
924
- person DM, whose message it is is already visually implied by side/alignment in most chat UIs, or at
925
- minimum isn't worth repeating every bubble — since this package doesn't currently do left/right
926
- alignment by sender, keep the bubble layout as-is and just suppress the redundant name text for DMs,
927
- which is the capability the row asks for, not a new alignment scheme).
928
-
929
- **Row 50 (per-recipient read-status detail, gated moderator + non-direct — PARTIAL, verify tooltip vs.
930
- jg's popover list):** `ReadTicks.jsx` already correctly gates the DM carve-out (`conversation?.kind ===
931
- 'direct' ? null : status.recipient_detail`, line 20) — **this part is already correct, do not touch
932
- the gating logic.** What's flagged PARTIAL is the *presentation*: a hover-only MUI `Tooltip` (line 22)
933
- is not touch/keyboard accessible the way jg's popover list is — a moderator on a touch device cannot
934
- see who read a message at all today. Convert to a click-to-open `Popover`/`Menu` showing the recipient
935
- names as a proper list (anchored on the existing `Typography` trigger), keeping the same content
936
- (`detailText`/aggregate `label`) — this is an interaction-fidelity fix, not new data plumbing.
937
-
938
- **Row 54 (poll option bounds 2-10 in the composer — verify):** dcm's server-side validation
939
- (`django-core-micha/src/django_core_micha/messaging/serializers.py:27`,
940
- `PollInputSerializer.options`) only enforces `min_length=2`, no maximum — the "2-10" bound is a
941
- client-only UX guard (jg's convention), not a server contract. `Composer.jsx`'s poll dialog
942
- (`pollOptions` state, `setPollOptions((current) => [...current, ''])`) currently allows unlimited
943
- options. Cap at 10: disable/hide the "Add option" button once `pollOptions.length >= 10`, and keep the
944
- existing minimum-2 validation as-is (already correct).
945
-
946
- **Deliverable: `docs/messaging-deviations.md` rewritten once, at the end of this chunk.** The WO's
947
- envelope is explicit that the doc's current "Final parity confirmation" section overclaims completeness
948
- and must be replaced with an honest, row-by-row account against this WO's full checklist (all 59 rows,
949
- not just chunks 1-6's 39 unblocked ones) — every row gets one of: `OK` (unchanged from before this WO,
950
- or newly landed), `DEV` (deliberate deviation with rationale — carry forward the existing ones from
951
- chunks 2/3/4/5 plus this chunk's row 6/42/47/50 interaction deviations), or `BLOCKED` (rows 38, 51-53,
952
- 56-58, plus row 27's now-recorded BLOCKED outcome from chunk 2, plus row 42's newly-recorded BLOCKED
953
- outcome from this chunk). This is the single most important deliverable of the whole WO per its own
954
- "why this WO exists" framing — do not compress it into a vague summary; go row by row, referencing the
955
- row number from the checklist in `work-orders/MSG-3b.md` Part A.
956
-
957
- **Correction from chunk 5's independent review, apply when writing row 36's deviation-doc entry:**
958
- chunk 5's guidance above (row 36 section) speculated the poll-disabled-while-images-staged gate exists
959
- "presumably because a message can't carry both a poll and attachments in one send." That premise is
960
- **not** true — verified against dcm's `services.py`: `send_message` and `create_poll` are always two
961
- structurally separate `Message` rows created via two separate API calls; there is no combined send that
962
- could carry both, so there is no server-side mutual-exclusivity constraint. The gate is real and kept
963
- (it's harmless UX convenience matching jg), but its deviation-doc rationale must say **"UI convenience,
964
- matching jg's composer behavior — no dcm-side constraint requires it,"** not the unverified premise
965
- above. Do not restate the wrong reasoning as fact — this is exactly the failure mode ("claimed
966
- completeness/reasoning without checking the seam") this WO exists to correct.
967
-
968
- **Required tests to WRITE (chunk 6 scope):**
969
- - Row 39: relative-timestamp rendering for a few representative ages (just-now, minutes, hours, days).
970
- - Row 40: only if genuinely fixing something (see the verify-first note above) — otherwise state the
971
- `already OK` conclusion in your summary instead of a padding test.
972
- - Row 45: a new message in the active conversation while scrolled near the bottom triggers a scroll-to-
973
- bottom call; while scrolled away from the bottom, it does not (assert the scroll call, not real
974
- jsdom layout, which has no real scroll metrics — mock `scrollHeight`/`scrollTop`/`clientHeight` on
975
- the container the way this environment's other scroll-adjacent tests already do, if any precedent
976
- exists, otherwise establish a minimal one here).
977
- - Row 46: the announcement link button renders only when a host callback is supplied and
978
- `link_target` is present; clicking it calls the host callback with the right value; absent either
979
- precondition, nothing extra renders.
980
- - Row 47: sender name renders in a group conversation, is suppressed in a direct conversation.
981
- - Row 50: the recipient-detail control is click-openable (not hover-only) and still respects the DM
982
- carve-out (no detail control rendered/openable at all for a direct conversation, even for a
983
- moderator) — this is a regression test for existing correct behavior, not just new UI.
984
- - Row 54: the "add option" control disables/hides at 10 options; existing 2-minimum validation stays
985
- covered by its current test (don't duplicate, just don't break it).
986
- - Existing ucm suites (chunks 1-5 inclusive) stay green.
987
-
988
- **Invariants / do-not-break:** don't touch chunks 1-5's code paths beyond the specific additive props
989
- described above (`conversation` on `MessageBubble`, `onAnnouncementLink` threading); don't invent a
990
- managed-all-vs-team distinction client-side (row 42's decision is BLOCKED/deviation, not a guess); keep
991
- the ~400 LOC soft trigger in mind across `Thread.jsx`/`MessageBubble.jsx`/`ConversationList.jsx` — this
992
- chunk touches all three, watch for any of them growing past the trigger and flag rather than silently
993
- letting it happen.
994
-
995
- **Progress contract / preamble:** identical to chunks 1-5's — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
996
- git operations, write-and-run-only-the-new-tests, leave the diff uncommitted.
997
-
998
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
999
- `work-orders/MSG-3b.md` chunk 6 (Part B above, after chunk 5 is committed) — the WO's last content
1000
- chunk. Follow `orchestrate-codex`; after this chunk's independent review and commit, run the WO-end
1001
- `ui_reviewer` pass (checking the rewritten deviation doc against the full checklist row by row per the
1002
- WO's "Reviews" section), then one version bump + npm publish.