@ai-matrx/messaging 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.3.0 — 2026-09-07
4
+
5
+ **Three more seams the first adoption needed** — each one a case where the host
6
+ would otherwise have kept a piece of its own messaging UI beside this package's,
7
+ which is the failure this package exists to prevent. Same session as 0.2.0.
8
+
9
+ ### Added
10
+
11
+ - **`wrapMessage` / `wrapConversationRow` — app chrome around a bubble and a row.**
12
+ The reason is the right-click menu: a platform whose every surface answers a right-click with
13
+ copy / export / attach / hand-to-an-agent cannot have messaging be the one surface that does
14
+ not, and that menu needs the app's surface name, entity tokens and clipboard primitive — none
15
+ of which a package can own. The wrapper WRAPS the package's bubble; it cannot replace it.
16
+ - **`renderReference` — the host's own reference renderer.** References are the one part of a
17
+ bubble an app is most likely to already render everywhere else (this platform resolves a
18
+ ```matrx fence through a kind registry into a live chip). Two treatments of one fence in one
19
+ app is the second-renderer defect. Omit it and the package's card is used, unchanged.
20
+ - **`onIncomingMessage` now receives an `IncomingMessageContext`** —
21
+ `{ isActiveConversation, conversation }`. "Do not interrupt someone for the conversation they
22
+ are looking at" is the rule every chat app needs and only this package can answer, and a
23
+ desktop notification names the SENDER, whose display name lives on a conversation the host
24
+ cannot see from above the provider. (The origin surface had `sender_id.substring(0, 8)` and a
25
+ `// Placeholder` comment where the name belonged.)
26
+
27
+ ### Consumer action (C28)
28
+
29
+ - `onIncomingMessage` gains a SECOND argument. Existing one-argument callbacks keep compiling and
30
+ keep working — but if you were suppressing notifications for the open conversation by reading
31
+ your own state, delete that and read `context.isActiveConversation`.
32
+ - Hosts with their own reference chips or per-row context menus: pass `renderReference` /
33
+ `wrapMessage` / `wrapConversationRow` and DELETE the host-side message list.
34
+
35
+ ## 0.2.0 — 2026-09-07
36
+
37
+ **Both fixes came from the first real adoption** (matrx-frontend, the C9 full-elimination swap).
38
+ Each is a case of the package forcing the host to keep something it should not have had to keep.
39
+
40
+ ### Fixed
41
+
42
+ - **The provider no longer builds a SECOND realtime manager under a host that already has one.**
43
+ `<MessagingProvider>` mounted `<RealtimeProvider>` unconditionally. An app that wires
44
+ `@ai-matrx/realtime` has exactly ONE provider on purpose — the manager owns the write ledger
45
+ that makes an optimistic write's own echo recognizable — so a second manager beneath it gave
46
+ this package its own ledger and its own channels: our sends looked remote to the host's
47
+ realtime consumers, and both sides subscribed to the same rooms twice. The provider now asks
48
+ `useIsRealtimeProviderMounted()` (`@ai-matrx/realtime` 0.3.0) and rides the host's manager when
49
+ there is one; an app that wires none still gets a provider for free, so R7 is unchanged.
50
+ Probing `useRealtimeManager() !== null` would NOT have worked — it is also null while a mounted
51
+ host provider waits for its Supabase client.
52
+ - **The React context moved into the `globalSlot`.** Module-scope `createContext` splits in two
53
+ under dual ESM/CJS evaluation, exactly like every other piece of module-level state this
54
+ package already slots.
55
+
56
+ ### Added
57
+
58
+ - **`actionRenderers` — a host surface for an action kind whose answer is a CARD, not a chip.**
59
+ Chips answer a question ("Approve" / "Decline"); a shared-resource card, a link out to a
60
+ report, an app-icon reminder are app-shaped surfaces only the host can draw. Without this seam
61
+ the first consumer had to keep its own message renderer beside this package's — a package plus
62
+ a surviving custom copy, which is the failure this package exists to prevent. The
63
+ forward-compatibility rule stays the PACKAGE's: an unknown kind, or a known kind at a version
64
+ the renderer does not list, renders nothing.
65
+ Types: `MessageActionRenderer`, `MessageActionRenderProps` from `@ai-matrx/messaging/react`.
66
+ - `src/react/host-integration.test.tsx` — four tests: the host's manager is the one messaging
67
+ uses (fails against 0.1.3), a provider-less app still gets one, a host renderer draws inside
68
+ the bubble, and an unlisted version renders nothing.
69
+
70
+ ### Consumer action (C28)
71
+
72
+ - **Requires `@ai-matrx/realtime` >= 0.3.0**, picked up automatically by the `"latest"` spec.
73
+ - Nothing to change in existing wiring. If your app mounts its own `<RealtimeProvider>`, messaging
74
+ now rides it — that is the fix, and it is what you wanted.
75
+ - Hosts with action kinds that render a card rather than a decision: pass them as
76
+ `actionRenderers={[{ kind, versions, render }]}` and DELETE the host-side bubble renderer.
77
+
3
78
  ## 0.1.3
4
79
 
5
80
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/README.md CHANGED
@@ -184,6 +184,19 @@ that fails when pressed.
184
184
  Omit it and reference cards render as labeled, non-interactive cards with a title explaining why —
185
185
  honest, never a button that does nothing.
186
186
 
187
+ ### Mounting it in an app that already wires realtime
188
+
189
+ Nothing to do. `<MessagingProvider>` rides the host's `<RealtimeProvider>` when one is mounted
190
+ above it, and mounts its own only when there is none. That is not a convenience — an app has ONE
191
+ realtime manager on purpose (it owns the write ledger that makes an optimistic write's own echo
192
+ recognizable), and a second one under it makes each manager's writes look remote to the other.
193
+
194
+ ```tsx
195
+ <RealtimeProvider client={supabase} actorId={user.id}> {/* the app's ONE mount */}
196
+ <MessagingProvider client={supabase} userId={user.id} organizationId={org.id}>
197
+
198
+ ```
199
+
187
200
  ### Registering an actionable message kind
188
201
 
189
202
  ```tsx
@@ -209,6 +222,46 @@ honest, never a button that does nothing.
209
222
  This is the seam `@ai-matrx/meet` uses: a call invitation is just a message with a structured
210
223
  payload.
211
224
 
225
+ ### An action kind whose answer is a card, not a chip
226
+
227
+ Chips answer a question. A shared-resource card, a link out to a report, a reminder with an app
228
+ icon are app-shaped surfaces only the host can draw — so the host supplies the surface and the
229
+ package still owns the gate:
230
+
231
+ ```tsx
232
+ <MessagingProvider … actionRenderers={[{
233
+ kind: "resource_shared",
234
+ versions: [1],
235
+ render: ({ payload, isOwn }) => <ResourceSharedCard payload={payload} isOwn={isOwn} />,
236
+ }]}>
237
+ ```
238
+
239
+ A kind with a renderer draws it instead of chips. Everything else is unchanged, including the
240
+ rule that matters: an unknown kind — or a known kind at a version this build does not list —
241
+ renders NOTHING. Use `actions` for decisions, `actionRenderers` for surfaces; a kind may have
242
+ both a handler (for `summarize`, used by previews) and a renderer.
243
+
244
+ ### Your app's chrome around our bubbles
245
+
246
+ A platform whose surfaces all answer a right-click, or that already renders references through
247
+ its own registry, hands those in rather than rebuilding the thread:
248
+
249
+ ```tsx
250
+ <MessagingProvider …
251
+ wrapMessage={({ message, children }) => (
252
+ <MessageContextMenu message={message}>{children}</MessageContextMenu>
253
+ )}
254
+ wrapConversationRow={({ conversation, children }) => (
255
+ <ConversationContextMenu conversation={conversation}>{children}</ConversationContextMenu>
256
+ )}
257
+ renderReference={({ reference }) => <AppReferenceChip reference={reference} />}
258
+ />
259
+ ```
260
+
261
+ `wrapMessage` and `wrapConversationRow` WRAP; they never replace. `renderReference` replaces the
262
+ package's card — deliberately, because an app that draws references everywhere else must not
263
+ draw them two ways.
264
+
212
265
  ### Theming
213
266
 
214
267
  Structural CSS ships in the package; the token **contract** is enforced; token **values** are
package/dist/react.cjs CHANGED
@@ -1780,7 +1780,12 @@ function createMessagingRepository(options) {
1780
1780
 
1781
1781
  // src/react/provider.tsx
1782
1782
  var import_jsx_runtime = require("react/jsx-runtime");
1783
- var MessagingContext = (0, import_react.createContext)(null);
1783
+ function messagingContext() {
1784
+ return globalSlot(
1785
+ "react-context",
1786
+ () => (0, import_react.createContext)(null)
1787
+ );
1788
+ }
1784
1789
  function defaultDiagnostics(event) {
1785
1790
  const line = `[@ai-matrx/messaging] ${event.message}${event.remedy !== void 0 ? `
1786
1791
  \u2192 ${event.remedy}` : ""}`;
@@ -1791,6 +1796,8 @@ function defaultDiagnostics(event) {
1791
1796
  function MessagingProvider(props) {
1792
1797
  const { client, userId, organizationId } = props;
1793
1798
  const ready = client != null && typeof userId === "string" && userId.length > 0 && typeof organizationId === "string" && organizationId.length > 0;
1799
+ const hostProvidesRealtime = (0, import_react2.useIsRealtimeProviderMounted)();
1800
+ if (hostProvidesRealtime) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessagingRuntime, { ...props });
1794
1801
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.RealtimeProvider, { client: ready ? client : null, actorId: userId ?? void 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessagingRuntime, { ...props }) });
1795
1802
  }
1796
1803
  function MessagingRuntime(props) {
@@ -1824,7 +1831,8 @@ function MessagingRuntime(props) {
1824
1831
  identity,
1825
1832
  ...props.resolveSession !== void 0 ? { resolveSession: props.resolveSession } : {}
1826
1833
  });
1827
- return createMessagingEngine({
1834
+ let built = null;
1835
+ built = createMessagingEngine({
1828
1836
  repository,
1829
1837
  manager,
1830
1838
  identity,
@@ -1833,8 +1841,18 @@ function MessagingRuntime(props) {
1833
1841
  onFallback: (message) => report({ level: "warn", message })
1834
1842
  }),
1835
1843
  onDiagnostic: report,
1836
- onIncoming: (message) => incomingRef.current?.(message)
1844
+ onIncoming: (message) => {
1845
+ const snapshot = built?.store.snapshot();
1846
+ if (snapshot === void 0) return;
1847
+ incomingRef.current?.(message, {
1848
+ isActiveConversation: snapshot.activeConversationId === message.conversationId,
1849
+ conversation: snapshot.conversations.find(
1850
+ (item) => item.conversation.id === message.conversationId
1851
+ ) ?? null
1852
+ });
1853
+ }
1837
1854
  });
1855
+ return built;
1838
1856
  }, [ready, manager, userId, organizationId]);
1839
1857
  (0, import_react.useEffect)(() => {
1840
1858
  if (engine === null) return void 0;
@@ -1859,6 +1877,17 @@ function MessagingRuntime(props) {
1859
1877
  agents
1860
1878
  });
1861
1879
  }, [transport, agents, ready, organizationId]);
1880
+ const renderers = props.actionRenderers;
1881
+ const rendererMap = (0, import_react.useMemo)(() => {
1882
+ const map = /* @__PURE__ */ new Map();
1883
+ (renderers ?? []).forEach((renderer) => {
1884
+ map.set(renderer.kind, renderer);
1885
+ });
1886
+ return map;
1887
+ }, [renderers]);
1888
+ const renderReference = props.renderReference ?? null;
1889
+ const wrapMessage = props.wrapMessage ?? null;
1890
+ const wrapConversationRow = props.wrapConversationRow ?? null;
1862
1891
  const host = (0, import_react.useMemo)(() => {
1863
1892
  if (engine === null) return null;
1864
1893
  return {
@@ -1866,13 +1895,18 @@ function MessagingRuntime(props) {
1866
1895
  actions: actionRegistry,
1867
1896
  ai,
1868
1897
  identity: engine.identity,
1869
- openReference: referenceRef.current ?? null
1898
+ openReference: referenceRef.current ?? null,
1899
+ actionRenderers: rendererMap,
1900
+ renderReference,
1901
+ wrapMessage,
1902
+ wrapConversationRow
1870
1903
  };
1871
- }, [engine, actionRegistry, ai]);
1904
+ }, [engine, actionRegistry, ai, rendererMap, renderReference, wrapMessage, wrapConversationRow]);
1905
+ const MessagingContext = messagingContext();
1872
1906
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessagingContext.Provider, { value: host, children });
1873
1907
  }
1874
1908
  function useMessagingHost() {
1875
- return (0, import_react.useContext)(MessagingContext);
1909
+ return (0, import_react.useContext)(messagingContext());
1876
1910
  }
1877
1911
  function useRequiredMessagingHost() {
1878
1912
  const host = useMessagingHost();
@@ -2538,6 +2572,7 @@ var AI_LABELS = {
2538
2572
  };
2539
2573
  function ConversationList(props) {
2540
2574
  const { conversations, hasMore, isInitialLoading, loadMore, select, activeConversationId } = useConversations();
2575
+ const RowChrome = useMessagingHost()?.wrapConversationRow ?? null;
2541
2576
  const [query, setQuery] = (0, import_react5.useState)("");
2542
2577
  const visible = (0, import_react5.useMemo)(() => {
2543
2578
  const needle = query.trim().toLowerCase();
@@ -2578,17 +2613,20 @@ function ConversationList(props) {
2578
2613
  title: query.length > 0 ? "No matches" : "No conversations yet",
2579
2614
  body: query.length > 0 ? "Try a different name or word." : "Start one and it will appear here."
2580
2615
  }
2581
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "mx-msg__rows", children: visible.map((item) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2582
- ConversationRow,
2583
- {
2584
- summary: item,
2585
- isActive: item.conversation.id === activeConversationId,
2586
- onSelect: () => {
2587
- select(item.conversation.id);
2588
- props.onSelect?.(item.conversation.id);
2616
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "mx-msg__rows", children: visible.map((item) => {
2617
+ const row = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2618
+ ConversationRow,
2619
+ {
2620
+ summary: item,
2621
+ isActive: item.conversation.id === activeConversationId,
2622
+ onSelect: () => {
2623
+ select(item.conversation.id);
2624
+ props.onSelect?.(item.conversation.id);
2625
+ }
2589
2626
  }
2590
- }
2591
- ) }, item.conversation.id)) }),
2627
+ );
2628
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: RowChrome !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowChrome, { conversation: item, children: row }) : row }, item.conversation.id);
2629
+ }) }),
2592
2630
  hasMore && !isInitialLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2593
2631
  "button",
2594
2632
  {
@@ -2817,6 +2855,8 @@ function MessageGroupView(props) {
2817
2855
  function MessageBubble(props) {
2818
2856
  const { message, isMine } = props;
2819
2857
  const host = useMessagingHost();
2858
+ const HostReference = host?.renderReference ?? null;
2859
+ const MessageChrome = host?.wrapMessage ?? null;
2820
2860
  if (message.deletedAt !== null) {
2821
2861
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mx-msg__bubble mx-msg__bubble--deleted", children: "Message deleted" });
2822
2862
  }
@@ -2827,10 +2867,16 @@ function MessageBubble(props) {
2827
2867
  message.deliveryState === "sending" ? "mx-msg__bubble--pending" : "",
2828
2868
  message.deliveryState === "failed" ? "mx-msg__bubble--failed" : ""
2829
2869
  ].filter(Boolean).join(" ");
2830
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2870
+ const bubble = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2831
2871
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: classes, children: [
2832
2872
  splitText(message.content).map(
2833
- (segment, index) => segment.type === "text" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: segment.value }, index) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2873
+ (segment, index) => segment.type === "text" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: segment.value }, index) : HostReference !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2874
+ HostReference,
2875
+ {
2876
+ reference: segment.reference
2877
+ },
2878
+ `${segment.reference.entityType}:${segment.reference.entityId}`
2879
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2834
2880
  ReferenceCard,
2835
2881
  {
2836
2882
  reference: segment.reference,
@@ -2839,14 +2885,22 @@ function MessageBubble(props) {
2839
2885
  `${segment.reference.entityType}:${segment.reference.entityId}`
2840
2886
  )
2841
2887
  ),
2842
- message.references.map((reference) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2843
- ReferenceCard,
2844
- {
2845
- reference,
2846
- onOpen: host?.openReference ?? null
2847
- },
2848
- `structured:${reference.entityType}:${reference.entityId}`
2849
- )),
2888
+ message.references.map(
2889
+ (reference) => HostReference !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2890
+ HostReference,
2891
+ {
2892
+ reference
2893
+ },
2894
+ `structured:${reference.entityType}:${reference.entityId}`
2895
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2896
+ ReferenceCard,
2897
+ {
2898
+ reference,
2899
+ onOpen: host?.openReference ?? null
2900
+ },
2901
+ `structured:${reference.entityType}:${reference.entityId}`
2902
+ )
2903
+ ),
2850
2904
  message.action !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MessageActionChips, { message }) : null
2851
2905
  ] }),
2852
2906
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -2882,9 +2936,25 @@ function MessageBubble(props) {
2882
2936
  }
2883
2937
  )
2884
2938
  ] });
2939
+ return MessageChrome !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MessageChrome, { message, isMine, children: bubble }) : bubble;
2885
2940
  }
2886
2941
  function MessageActionChips(props) {
2942
+ const host = useRequiredMessagingHost();
2887
2943
  const action = useMessageAction(props.message);
2944
+ const declared = props.message.action;
2945
+ const renderer = declared === null ? void 0 : host.actionRenderers.get(declared.kind);
2946
+ if (declared !== null && renderer !== void 0) {
2947
+ if (!renderer.versions.includes(declared.version)) return null;
2948
+ const Render = renderer.render;
2949
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2950
+ Render,
2951
+ {
2952
+ message: props.message,
2953
+ payload: declared.payload,
2954
+ isOwn: props.message.senderId === host.identity.userId
2955
+ }
2956
+ );
2957
+ }
2888
2958
  if (action.receipt !== null) {
2889
2959
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "mx-msg__receipt", children: [
2890
2960
  action.receipt.label,