@ai-matrx/messaging 0.0.0 → 0.1.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/dist/react.js ADDED
@@ -0,0 +1,2980 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/react/provider.tsx
5
+ import {
6
+ createContext,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useSyncExternalStore
12
+ } from "react";
13
+ import { RealtimeProvider, useRealtimeManager } from "@ai-matrx/realtime/react";
14
+
15
+ // src/core/actions.ts
16
+ function receiptKey(kind, messageId, actorId) {
17
+ return `${kind}::${messageId}::${actorId}`;
18
+ }
19
+ function createActionRegistry() {
20
+ const handlers = /* @__PURE__ */ new Map();
21
+ const receipts = /* @__PURE__ */ new Map();
22
+ const inFlight = /* @__PURE__ */ new Map();
23
+ const registry = {
24
+ register(handler) {
25
+ handlers.set(handler.kind, handler);
26
+ },
27
+ resolve(action) {
28
+ const handler = handlers.get(action.kind);
29
+ if (handler === void 0) return null;
30
+ return handler.versions.includes(action.version) ? handler : null;
31
+ },
32
+ choicesFor(action) {
33
+ const handler = registry.resolve(action);
34
+ return handler === null ? [] : handler.choices(action.payload);
35
+ },
36
+ summarize(action) {
37
+ const handler = registry.resolve(action);
38
+ if (handler?.summarize === void 0) return null;
39
+ return handler.summarize(action.payload);
40
+ },
41
+ execute(action, context) {
42
+ const key = receiptKey(action.kind, context.messageId, context.actorId);
43
+ const settled = receipts.get(key);
44
+ if (settled !== void 0) return Promise.resolve(settled);
45
+ const running = inFlight.get(key);
46
+ if (running !== void 0) return running;
47
+ const handler = registry.resolve(action);
48
+ if (handler === null) {
49
+ const receipt = {
50
+ kind: action.kind,
51
+ messageId: context.messageId,
52
+ actorId: context.actorId,
53
+ outcome: "unavailable",
54
+ label: "Not available in this app version",
55
+ settledAt: (/* @__PURE__ */ new Date()).toISOString(),
56
+ detail: `No handler registered for "${action.kind}" v${action.version}. Update the app, or register a handler on <MessagingProvider actions={...}>.`
57
+ };
58
+ return Promise.resolve(receipt);
59
+ }
60
+ const started = handler.execute(action.payload, context).then((receipt) => {
61
+ if (receipt.outcome === "applied" || receipt.outcome === "already") {
62
+ receipts.set(key, receipt);
63
+ }
64
+ return receipt;
65
+ }).finally(() => {
66
+ inFlight.delete(key);
67
+ });
68
+ inFlight.set(key, started);
69
+ return started;
70
+ },
71
+ receiptFor(kind, messageId, actorId) {
72
+ return receipts.get(receiptKey(kind, messageId, actorId)) ?? null;
73
+ },
74
+ observeReceipt(receipt) {
75
+ if (receipt.outcome === "applied" || receipt.outcome === "already") {
76
+ receipts.set(receiptKey(receipt.kind, receipt.messageId, receipt.actorId), receipt);
77
+ }
78
+ },
79
+ known() {
80
+ return [...handlers.keys()];
81
+ }
82
+ };
83
+ return registry;
84
+ }
85
+
86
+ // src/core/ai.ts
87
+ import {
88
+ newEphemeralConversationStart,
89
+ runAgentToCompletion
90
+ } from "@ai-matrx/agents/matrx";
91
+
92
+ // src/core/errors.ts
93
+ var MessagingError = class extends Error {
94
+ code;
95
+ /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
96
+ remedy;
97
+ cause;
98
+ constructor(code, message, remedy, cause) {
99
+ super(message);
100
+ this.name = "MessagingError";
101
+ this.code = code;
102
+ this.remedy = remedy;
103
+ if (cause !== void 0) this.cause = cause;
104
+ }
105
+ /** True when retrying after the host re-establishes a session is the fix. */
106
+ get isRetryable() {
107
+ return this.code === "session-unavailable" || this.code === "transport";
108
+ }
109
+ };
110
+ var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
111
+ var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
112
+ var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
113
+ var SESSION_MARKERS = [
114
+ "auth session missing",
115
+ "jwt expired",
116
+ "no api key found",
117
+ "refresh_token_not_found",
118
+ "invalid claim: missing sub claim"
119
+ ];
120
+ function normalizeMessagingError(error, operation) {
121
+ if (error instanceof MessagingError) return error;
122
+ const raw = error;
123
+ const message = typeof raw?.message === "string" ? raw.message : String(error);
124
+ const code = typeof raw?.code === "string" ? raw.code : void 0;
125
+ const lowered = message.toLowerCase();
126
+ if (raw?.name === "SessionUnavailableError" || SESSION_MARKERS.some((marker) => lowered.includes(marker))) {
127
+ return new MessagingError(
128
+ "session-unavailable",
129
+ `${operation}: no Supabase session available (${message})`,
130
+ "Normal during sign-in and token refresh. The package retries once after the host's session source resolves; log it as a warning, never as an error.",
131
+ error
132
+ );
133
+ }
134
+ if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
135
+ return new MessagingError(
136
+ "forbidden",
137
+ `${operation}: denied by the database (${code}: ${message})`,
138
+ "Authorization is RLS + auth-checked RPCs (R5). Fix the policy or the caller's membership \u2014 never work around it with a service-role client in a browser.",
139
+ error
140
+ );
141
+ }
142
+ if (code !== void 0 && NOT_FOUND_CODES.has(code)) {
143
+ return new MessagingError(
144
+ "not-found",
145
+ `${operation}: not found (${code}: ${message})`,
146
+ "The row is gone, or RLS hides it from this user. Re-read the conversation list.",
147
+ error
148
+ );
149
+ }
150
+ if (code !== void 0 && CONFLICT_CODES.has(code)) {
151
+ return new MessagingError(
152
+ "conflict",
153
+ `${operation}: unique violation (${code}: ${message})`,
154
+ "A concurrent writer won. For messages this is the client_message_id idempotency key doing its job \u2014 re-read the row rather than retrying the insert.",
155
+ error
156
+ );
157
+ }
158
+ return new MessagingError(
159
+ "transport",
160
+ `${operation}: ${message}`,
161
+ "Transient transport failure. The package retries reads; a write is surfaced to the outbox so the typed message is never lost.",
162
+ error
163
+ );
164
+ }
165
+ function invalidResponse(operation, detail) {
166
+ return new MessagingError(
167
+ "invalid-response",
168
+ `${operation}: ${detail}`,
169
+ "The database returned a shape this package does not accept. Rendering it would put a lie on the screen, so it is refused here. Fix the RPC's return contract."
170
+ );
171
+ }
172
+
173
+ // src/core/references.ts
174
+ var FENCE = /```matrx\s*\n([\s\S]*?)\n?```/g;
175
+ function parseFenceBody(body) {
176
+ let parsed;
177
+ try {
178
+ parsed = JSON.parse(body);
179
+ } catch {
180
+ return [];
181
+ }
182
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
183
+ const references = [];
184
+ for (const entry of entries) {
185
+ if (typeof entry !== "object" || entry === null) continue;
186
+ const record = entry;
187
+ const entityType = record["entityType"] ?? record["entity_type"] ?? record["type"];
188
+ const entityId = record["entityId"] ?? record["entity_id"] ?? record["id"];
189
+ if (typeof entityType !== "string" || typeof entityId !== "string") continue;
190
+ if (entityType.length === 0 || entityId.length === 0) continue;
191
+ const label = record["label"] ?? record["title"] ?? record["name"];
192
+ const href = record["href"] ?? record["url"];
193
+ references.push({
194
+ entityType,
195
+ entityId,
196
+ label: typeof label === "string" && label.length > 0 ? label : entityId,
197
+ ...typeof href === "string" && href.length > 0 ? { href } : {}
198
+ });
199
+ }
200
+ return references;
201
+ }
202
+ function extractReferences(content, structured = []) {
203
+ const seen = /* @__PURE__ */ new Map();
204
+ const add = (reference) => {
205
+ seen.set(`${reference.entityType}:${reference.entityId}`, reference);
206
+ };
207
+ structured.forEach(add);
208
+ for (const match of content.matchAll(FENCE)) {
209
+ parseFenceBody(match[1] ?? "").forEach(add);
210
+ }
211
+ return [...seen.values()];
212
+ }
213
+ function splitText(content) {
214
+ const segments = [];
215
+ let cursor = 0;
216
+ for (const match of content.matchAll(FENCE)) {
217
+ const start = match.index ?? 0;
218
+ if (start > cursor) {
219
+ segments.push({ type: "text", value: content.slice(cursor, start) });
220
+ }
221
+ parseFenceBody(match[1] ?? "").forEach((reference) => {
222
+ segments.push({ type: "reference", reference });
223
+ });
224
+ cursor = start + match[0].length;
225
+ }
226
+ if (cursor < content.length) {
227
+ segments.push({ type: "text", value: content.slice(cursor) });
228
+ }
229
+ return segments.filter(
230
+ (segment) => segment.type === "reference" || segment.value.trim().length > 0
231
+ );
232
+ }
233
+ function summarizeText(content, maxLength = 140) {
234
+ const parts = splitText(content).map(
235
+ (segment) => segment.type === "text" ? segment.value : segment.reference.label
236
+ );
237
+ const flattened = parts.join(" ").replace(/\s+/g, " ").trim();
238
+ if (flattened.length <= maxLength) return flattened;
239
+ return `${flattened.slice(0, maxLength - 1).trimEnd()}\u2026`;
240
+ }
241
+ function composeFence(references) {
242
+ if (references.length === 0) return "";
243
+ return `\`\`\`matrx
244
+ ${JSON.stringify(references, null, 2)}
245
+ \`\`\``;
246
+ }
247
+
248
+ // src/core/ai.ts
249
+ function nameOf(participants, senderId) {
250
+ return participants.find((p) => p.userId === senderId)?.displayName ?? senderId;
251
+ }
252
+ function buildTranscript(messages, participants, limit, since) {
253
+ const relevant = messages.filter((message) => {
254
+ if (message.deletedAt !== null) return false;
255
+ if (since === null) return true;
256
+ return message.createdAt > since;
257
+ });
258
+ const windowed = relevant.slice(-limit);
259
+ return windowed.map((message) => ({
260
+ at: message.createdAt,
261
+ author: nameOf(participants, message.senderId),
262
+ // The transcript is TEXT. A reference fence becomes its label, never JSON —
263
+ // the same collapse the inbox preview uses.
264
+ text: summarizeText(message.content, 2e3)
265
+ }));
266
+ }
267
+ function createMessagingAi(options) {
268
+ const limit = options.maxTranscriptMessages ?? 200;
269
+ function agentFor(capability) {
270
+ const agentId = options.agents[capability];
271
+ if (typeof agentId !== "string" || agentId.length === 0) {
272
+ throw new MessagingError(
273
+ "misconfigured",
274
+ `Messaging AI capability "${capability}" has no agent configured`,
275
+ `Pass agents={{ ${capability}: "<agent-id>" }} to <MessagingProvider>. Agent definitions live in the database, never in this package \u2014 the id is the only part a host injects. Until then the UI hides this action rather than offering a button that cannot work.`
276
+ );
277
+ }
278
+ return agentId;
279
+ }
280
+ async function run(capability, variables, userInput, signal) {
281
+ const agentId = agentFor(capability);
282
+ const completed = await runAgentToCompletion(
283
+ options.transport,
284
+ agentId,
285
+ {
286
+ ...newEphemeralConversationStart(),
287
+ organization_id: options.organizationId,
288
+ source_app: options.sourceApp ?? "ai-matrx",
289
+ source_feature: options.sourceFeature ?? `messaging.${capability}`,
290
+ initiation: "user",
291
+ // THE USER-INPUT LAW: structured content is NEVER here.
292
+ ...userInput !== null ? { user_input: userInput } : {},
293
+ variables
294
+ },
295
+ signal !== void 0 ? { signal } : {}
296
+ );
297
+ return {
298
+ capability,
299
+ text: completed.text.trim(),
300
+ conversationId: completed.conversationId
301
+ };
302
+ }
303
+ function variablesFor(args) {
304
+ const transcript = buildTranscript(
305
+ args.messages,
306
+ args.participants,
307
+ limit,
308
+ args.since ?? null
309
+ );
310
+ return {
311
+ conversation_id: args.conversationId,
312
+ participants: args.participants.map((participant) => ({
313
+ user_id: participant.userId,
314
+ display_name: participant.displayName,
315
+ is_agent: participant.isAgent
316
+ })),
317
+ transcript: transcript.map((entry) => ({
318
+ at: entry.at,
319
+ author: entry.author,
320
+ text: entry.text
321
+ })),
322
+ transcript_message_count: transcript.length,
323
+ // Honest about the cut, so an agent can say "showing the last 200".
324
+ transcript_truncated: args.messages.length > transcript.length,
325
+ ...args.since != null ? { unread_since: args.since } : {}
326
+ };
327
+ }
328
+ return {
329
+ available: () => ["catchUp", "summarize", "actionItems", "draftReply"].filter(
330
+ (capability) => {
331
+ const id = options.agents[capability];
332
+ return typeof id === "string" && id.length > 0;
333
+ }
334
+ ),
335
+ isAvailable(capability) {
336
+ const id = options.agents[capability];
337
+ return typeof id === "string" && id.length > 0;
338
+ },
339
+ catchMeUp: (args) => run("catchUp", variablesFor(args), null, args.signal),
340
+ summarize: (args) => run("summarize", variablesFor(args), null, args.signal),
341
+ extractActionItems: (args) => run("actionItems", variablesFor(args), null, args.signal),
342
+ draftReply: (args) => run(
343
+ "draftReply",
344
+ variablesFor(args),
345
+ // The ONE genuine human utterance in this module: what the user asked
346
+ // the drafter for. Everything else rode `variables`.
347
+ args.instruction !== void 0 && args.instruction.trim().length > 0 ? args.instruction.trim() : null,
348
+ args.signal
349
+ )
350
+ };
351
+ }
352
+
353
+ // src/core/engine.ts
354
+ import {
355
+ clientSessionId
356
+ } from "@ai-matrx/realtime";
357
+
358
+ // src/core/channels.ts
359
+ import { defineChannelNamespace } from "@ai-matrx/realtime";
360
+
361
+ // src/core/slot.ts
362
+ var NAMESPACE = "ai-matrx.messaging";
363
+ function globalSlot(name, create) {
364
+ const key = /* @__PURE__ */ Symbol.for(`${NAMESPACE}.${name}`);
365
+ const host = globalThis;
366
+ const existing = host[key];
367
+ if (existing !== void 0) return existing;
368
+ const created = create();
369
+ host[key] = created;
370
+ return created;
371
+ }
372
+
373
+ // src/core/channels.ts
374
+ function namespaces() {
375
+ return globalSlot("channel-namespaces", () => ({
376
+ inbox: defineChannelNamespace({
377
+ namespace: "messaging-inbox",
378
+ parts: ["userId"],
379
+ description: "One user's conversation list: new messages anywhere they participate, membership changes, and read-state updates."
380
+ }),
381
+ conversation: defineChannelNamespace({
382
+ namespace: "messaging-conversation",
383
+ parts: ["conversationId"],
384
+ description: "One conversation: message inserts/updates, presence, and typing \u2014 deliberately one channel, because they are one room."
385
+ })
386
+ }));
387
+ }
388
+ function inboxTopic(userId) {
389
+ return namespaces().inbox.topic({ userId });
390
+ }
391
+ function conversationTopic(conversationId) {
392
+ return namespaces().conversation.topic({ conversationId });
393
+ }
394
+ var MESSAGING_EVENTS = {
395
+ /** A freshly sent message, broadcast beside the Postgres Changes row so a
396
+ * receiver gets it on whichever path arrives first (both are deduped). */
397
+ message: "mx.message",
398
+ /** A message edited or soft-deleted. */
399
+ messageUpdated: "mx.message.updated",
400
+ /** An action receipt, so every viewer's chip settles at once. */
401
+ actionSettled: "mx.action.settled"
402
+ };
403
+
404
+ // src/core/outbox.ts
405
+ import { randomId } from "@ai-matrx/realtime";
406
+ function createMemoryOutboxStorage() {
407
+ let held = [];
408
+ return {
409
+ name: "memory",
410
+ durable: false,
411
+ load: () => held,
412
+ save: (entries) => {
413
+ held = entries;
414
+ }
415
+ };
416
+ }
417
+ function createWebOutboxStorage(args) {
418
+ const key = args.key ?? "ai-matrx.messaging.outbox";
419
+ let storage = args.storage ?? null;
420
+ if (storage === null) {
421
+ try {
422
+ const candidate = globalThis.localStorage;
423
+ storage = candidate ?? null;
424
+ } catch {
425
+ storage = null;
426
+ }
427
+ }
428
+ if (storage === null) {
429
+ args.onFallback?.(
430
+ "localStorage is unavailable, so queued messages will NOT survive a reload. Inject an OutboxStorage on <MessagingProvider> to restore durability."
431
+ );
432
+ return createMemoryOutboxStorage();
433
+ }
434
+ const backing = storage;
435
+ return {
436
+ name: "web-storage",
437
+ durable: true,
438
+ load() {
439
+ try {
440
+ const raw = backing.getItem(key);
441
+ if (raw === null) return [];
442
+ const parsed = JSON.parse(raw);
443
+ return Array.isArray(parsed) ? parsed : [];
444
+ } catch {
445
+ return [];
446
+ }
447
+ },
448
+ save(entries) {
449
+ try {
450
+ backing.setItem(key, JSON.stringify(entries));
451
+ } catch {
452
+ args.onFallback?.(
453
+ "Writing the outbox to localStorage failed (quota or private mode); queued messages are in memory only for this session."
454
+ );
455
+ }
456
+ }
457
+ };
458
+ }
459
+ var DEFAULT_BACKOFF = [0, 1e3, 3e3, 8e3, 2e4];
460
+ function createOutbox(options) {
461
+ const timers = options.timers ?? {
462
+ setTimeout: (run, ms) => setTimeout(run, ms),
463
+ clearTimeout: (handle) => clearTimeout(handle),
464
+ now: () => Date.now()
465
+ };
466
+ const storage = options.storage ?? createMemoryOutboxStorage();
467
+ const maxAttempts = options.maxAttempts ?? DEFAULT_BACKOFF.length;
468
+ const backoff = options.backoffMs ?? DEFAULT_BACKOFF;
469
+ let entries = [...storage.load()].map((entry) => ({
470
+ ...entry,
471
+ // Anything found mid-`sending` after a reload is genuinely unknown: it may
472
+ // or may not have reached the database. It goes back to `queued` and the
473
+ // idempotency key makes the re-send safe — that is exactly what the key is
474
+ // for. Marking it failed instead would strand a message that was typed.
475
+ state: entry.state === "sending" ? "queued" : entry.state
476
+ }));
477
+ let timer = null;
478
+ let disposed = false;
479
+ function persist() {
480
+ storage.save(entries);
481
+ options.onChange(entries);
482
+ }
483
+ function delayFor(attempts) {
484
+ return backoff[Math.min(attempts, backoff.length - 1)] ?? 0;
485
+ }
486
+ function schedule(ms) {
487
+ if (disposed || timer !== null) return;
488
+ timer = timers.setTimeout(() => {
489
+ timer = null;
490
+ void pump();
491
+ }, ms);
492
+ }
493
+ async function pump() {
494
+ if (disposed) return;
495
+ const next = entries.find((entry) => entry.state === "queued");
496
+ if (next === void 0) return;
497
+ entries = entries.map(
498
+ (entry) => entry.id === next.id ? { ...entry, state: "sending" } : entry
499
+ );
500
+ persist();
501
+ try {
502
+ const message = await options.send(next.draft, next.clientMessageId);
503
+ entries = entries.filter((entry) => entry.id !== next.id);
504
+ persist();
505
+ options.onSent(next, message);
506
+ schedule(0);
507
+ } catch (error) {
508
+ const attempts = next.attempts + 1;
509
+ const reason = error instanceof MessagingError ? error.message : String(error?.message ?? error);
510
+ const exhausted = attempts >= maxAttempts;
511
+ entries = entries.map(
512
+ (entry) => entry.id === next.id ? {
513
+ ...entry,
514
+ attempts,
515
+ state: exhausted ? "failed" : "queued",
516
+ failureReason: reason
517
+ } : entry
518
+ );
519
+ persist();
520
+ if (exhausted) {
521
+ options.onDiagnostic?.(
522
+ `Message could not be sent after ${attempts} attempts: ${reason}. It is still in the outbox \u2014 offer the user Retry or Discard; never drop it.`
523
+ );
524
+ schedule(0);
525
+ } else {
526
+ schedule(delayFor(attempts));
527
+ }
528
+ }
529
+ }
530
+ const outbox = {
531
+ entries: () => entries,
532
+ enqueue(draft) {
533
+ const clientMessageId = `mx-${randomId()}`;
534
+ entries = [
535
+ ...entries,
536
+ {
537
+ id: randomId(),
538
+ clientMessageId,
539
+ draft,
540
+ queuedAt: timers.now(),
541
+ attempts: 0,
542
+ state: "queued",
543
+ failureReason: null
544
+ }
545
+ ];
546
+ persist();
547
+ schedule(0);
548
+ return clientMessageId;
549
+ },
550
+ retry(entryId) {
551
+ entries = entries.map(
552
+ (entry) => entry.id === entryId ? { ...entry, state: "queued", attempts: 0, failureReason: null } : entry
553
+ );
554
+ persist();
555
+ schedule(0);
556
+ },
557
+ discard(entryId) {
558
+ entries = entries.filter((entry) => entry.id !== entryId);
559
+ persist();
560
+ },
561
+ flush() {
562
+ entries = entries.map(
563
+ (entry) => entry.state === "failed" ? { ...entry, state: "queued", attempts: 0 } : entry
564
+ );
565
+ persist();
566
+ schedule(0);
567
+ },
568
+ pendingFor(conversationId) {
569
+ return entries.filter((entry) => entry.draft.conversationId === conversationId);
570
+ },
571
+ dispose() {
572
+ disposed = true;
573
+ if (timer !== null) timers.clearTimeout(timer);
574
+ timer = null;
575
+ }
576
+ };
577
+ if (entries.length > 0) {
578
+ options.onDiagnostic?.(
579
+ `Restored ${entries.length} unsent message(s) from the ${storage.name} outbox.`
580
+ );
581
+ schedule(0);
582
+ }
583
+ return outbox;
584
+ }
585
+
586
+ // src/core/projection.ts
587
+ function str(row, key) {
588
+ const value = row[key];
589
+ return typeof value === "string" && value.length > 0 ? value : null;
590
+ }
591
+ function requiredStr(row, key, operation) {
592
+ const value = str(row, key);
593
+ if (value === null) {
594
+ throw invalidResponse(operation, `required field "${key}" was ${JSON.stringify(row[key])}`);
595
+ }
596
+ return value;
597
+ }
598
+ function bool(row, key, fallback) {
599
+ const value = row[key];
600
+ return typeof value === "boolean" ? value : fallback;
601
+ }
602
+ function jsonObject(value) {
603
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
604
+ }
605
+ var CONVERSATION_TYPES = /* @__PURE__ */ new Set(["direct", "group", "org"]);
606
+ var MESSAGE_KINDS = /* @__PURE__ */ new Set([
607
+ "text",
608
+ "image",
609
+ "video",
610
+ "audio",
611
+ "file",
612
+ "system",
613
+ "action"
614
+ ]);
615
+ var ROLES = /* @__PURE__ */ new Set(["owner", "admin", "member"]);
616
+ var DELIVERY_STATES = /* @__PURE__ */ new Set([
617
+ "sending",
618
+ "sent",
619
+ "delivered",
620
+ "read",
621
+ "failed"
622
+ ]);
623
+ function projectUserSummary(row) {
624
+ const userId = requiredStr(row, "user_id", "projectUserSummary");
625
+ const displayName = str(row, "display_name") ?? str(row, "email") ?? userId;
626
+ return {
627
+ userId,
628
+ displayName,
629
+ email: str(row, "email"),
630
+ avatarUrl: str(row, "avatar_url"),
631
+ isAgent: bool(row, "is_agent", false)
632
+ };
633
+ }
634
+ function projectParticipants(value, operation) {
635
+ if (value === null || value === void 0) return [];
636
+ if (!Array.isArray(value)) {
637
+ throw invalidResponse(
638
+ operation,
639
+ `"participants" was ${typeof value}, not an array \u2014 the RPC's jsonb aggregate is malformed`
640
+ );
641
+ }
642
+ const summaries = [];
643
+ for (const entry of value) {
644
+ if (typeof entry !== "object" || entry === null) continue;
645
+ const record = entry;
646
+ const id = str(record, "user_id") ?? str(record, "id");
647
+ if (id === null) continue;
648
+ summaries.push(
649
+ projectUserSummary({
650
+ ...record,
651
+ user_id: id
652
+ })
653
+ );
654
+ }
655
+ return summaries;
656
+ }
657
+ function projectMessageAction(value) {
658
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
659
+ const record = value;
660
+ const kind = record["kind"];
661
+ if (typeof kind !== "string" || kind.length === 0) return null;
662
+ const version = record["version"];
663
+ return {
664
+ kind,
665
+ // A payload without a version is version 1 — the shape that predates the
666
+ // envelope. Refusing it would silently blank every message sent before the
667
+ // envelope existed.
668
+ version: typeof version === "number" && Number.isFinite(version) ? version : 1,
669
+ payload: jsonObject(record["payload"])
670
+ };
671
+ }
672
+ function projectAttachments(metadata) {
673
+ const raw = metadata["attachments"];
674
+ if (!Array.isArray(raw)) return [];
675
+ const attachments = [];
676
+ for (const entry of raw) {
677
+ if (typeof entry !== "object" || entry === null) continue;
678
+ const record = entry;
679
+ const fileId = str(record, "fileId") ?? str(record, "file_id");
680
+ if (fileId === null) continue;
681
+ const size = record["sizeBytes"] ?? record["size_bytes"];
682
+ const width = record["width"];
683
+ const height = record["height"];
684
+ attachments.push({
685
+ fileId,
686
+ fileName: str(record, "fileName") ?? str(record, "file_name") ?? fileId,
687
+ mimeType: str(record, "mimeType") ?? str(record, "mime_type"),
688
+ sizeBytes: typeof size === "number" ? size : null,
689
+ width: typeof width === "number" ? width : null,
690
+ height: typeof height === "number" ? height : null
691
+ });
692
+ }
693
+ return attachments;
694
+ }
695
+ function projectReferences(metadata) {
696
+ const raw = metadata["references"];
697
+ if (!Array.isArray(raw)) return [];
698
+ const references = [];
699
+ for (const entry of raw) {
700
+ if (typeof entry !== "object" || entry === null) continue;
701
+ const record = entry;
702
+ const entityType = str(record, "entityType") ?? str(record, "entity_type");
703
+ const entityId = str(record, "entityId") ?? str(record, "entity_id");
704
+ if (entityType === null || entityId === null) continue;
705
+ const href = str(record, "href");
706
+ references.push({
707
+ entityType,
708
+ entityId,
709
+ label: str(record, "label") ?? entityId,
710
+ ...href !== null ? { href } : {}
711
+ });
712
+ }
713
+ return references;
714
+ }
715
+ function projectMessage(row, fallbackOrganizationId) {
716
+ const operation = "projectMessage";
717
+ const metadata = jsonObject(row["metadata"]);
718
+ const kindRaw = str(row, "message_type") ?? "text";
719
+ const stateRaw = str(row, "status") ?? "sent";
720
+ const replyTo = str(row, "reply_to_id");
721
+ const clientMessageId = str(row, "client_message_id");
722
+ const action = projectMessageAction(row["action_data"]);
723
+ return {
724
+ id: requiredStr(row, "id", operation),
725
+ conversationId: requiredStr(row, "conversation_id", operation),
726
+ senderId: requiredStr(row, "sender_id", operation),
727
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
728
+ content: typeof row["content"] === "string" ? row["content"] : "",
729
+ kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
730
+ // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
731
+ // states and can never be projected from a persisted row.
732
+ deliveryState: DELIVERY_STATES.has(stateRaw) && stateRaw !== "sending" && stateRaw !== "failed" ? stateRaw : "sent",
733
+ replyToId: replyTo === null ? null : replyTo,
734
+ clientMessageId: clientMessageId === null ? null : clientMessageId,
735
+ createdAt: requiredStr(row, "created_at", operation),
736
+ editedAt: str(row, "edited_at"),
737
+ deletedAt: str(row, "deleted_at"),
738
+ deletedForEveryone: bool(row, "deleted_for_everyone", false),
739
+ action,
740
+ attachments: projectAttachments(metadata),
741
+ references: projectReferences(metadata),
742
+ metadata
743
+ };
744
+ }
745
+ function projectParticipantRole(value) {
746
+ return typeof value === "string" && ROLES.has(value) ? value : "member";
747
+ }
748
+ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
749
+ const operation = "projectConversationSummary";
750
+ const id = requiredStr(row, "conversation_id", operation);
751
+ const typeRaw = str(row, "conversation_type") ?? "direct";
752
+ const type = CONVERSATION_TYPES.has(typeRaw) ? typeRaw : "direct";
753
+ const participants = projectParticipants(row["participants"], operation);
754
+ const groupName = str(row, "group_name");
755
+ const groupImageUrl = str(row, "group_image_url");
756
+ const createdBy = str(row, "created_by");
757
+ const updatedAt = str(row, "conversation_updated_at") ?? str(row, "updated_at");
758
+ const createdAt = str(row, "conversation_created_at") ?? str(row, "created_at") ?? updatedAt;
759
+ const lastMessageAt = str(row, "last_message_at");
760
+ const lastSender = str(row, "last_message_sender_id");
761
+ const unreadRaw = row["unread_count"];
762
+ if (createdAt === null || updatedAt === null) {
763
+ throw invalidResponse(operation, `conversation ${id} carried no timestamps`);
764
+ }
765
+ const others = participants.filter((participant) => participant.userId !== viewerId);
766
+ const displayName = type === "direct" ? others[0]?.displayName ?? "Direct message" : groupName ?? "Group conversation";
767
+ const displayImageUrl = type === "direct" ? others[0]?.avatarUrl ?? null : groupImageUrl;
768
+ return {
769
+ conversation: {
770
+ id,
771
+ type,
772
+ groupName,
773
+ groupImageUrl,
774
+ createdBy: createdBy === null ? null : createdBy,
775
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
776
+ createdAt,
777
+ updatedAt,
778
+ metadata: jsonObject(row["metadata"])
779
+ },
780
+ participants,
781
+ lastMessageContent: str(row, "last_message_content"),
782
+ lastMessageSenderId: lastSender === null ? null : lastSender,
783
+ lastMessageAt,
784
+ unreadCount: typeof unreadRaw === "number" && Number.isFinite(unreadRaw) && unreadRaw > 0 ? Math.floor(unreadRaw) : 0,
785
+ isMuted: bool(row, "is_muted", false),
786
+ isArchived: bool(row, "is_archived", false),
787
+ displayName,
788
+ displayImageUrl,
789
+ // The keyset sort value: the last message if there is one, else the
790
+ // conversation's own update stamp. Empty conversations must still sort.
791
+ sortAt: lastMessageAt ?? updatedAt
792
+ };
793
+ }
794
+
795
+ // src/core/store.ts
796
+ function timeOf(message) {
797
+ const stamp = message.editedAt ?? message.createdAt;
798
+ const parsed = Date.parse(stamp);
799
+ return Number.isFinite(parsed) ? parsed : 0;
800
+ }
801
+ function compare(a, b) {
802
+ if (a.createdAt !== b.createdAt) return a.createdAt < b.createdAt ? -1 : 1;
803
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
804
+ }
805
+ function sameMessage(a, b) {
806
+ if (a.id === b.id) return true;
807
+ const key = b.clientMessageId;
808
+ return key !== null && a.clientMessageId === key;
809
+ }
810
+ function insertOrdered(messages, message) {
811
+ const next = [...messages];
812
+ let index = next.length;
813
+ while (index > 0) {
814
+ const candidate = next[index - 1];
815
+ if (candidate === void 0 || compare(candidate, message) <= 0) break;
816
+ index -= 1;
817
+ }
818
+ next.splice(index, 0, message);
819
+ return next;
820
+ }
821
+ function createMessagingStore() {
822
+ let conversations = [];
823
+ let hasMoreConversations = false;
824
+ let hasLoadedConversations = false;
825
+ let threads = /* @__PURE__ */ new Map();
826
+ let activeConversationId = null;
827
+ const listeners = /* @__PURE__ */ new Set();
828
+ let cached = null;
829
+ function snapshot() {
830
+ if (cached !== null) return cached;
831
+ cached = {
832
+ conversations,
833
+ hasMoreConversations,
834
+ hasLoadedConversations,
835
+ threads,
836
+ activeConversationId,
837
+ totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length
838
+ };
839
+ return cached;
840
+ }
841
+ function emit() {
842
+ cached = null;
843
+ const next = snapshot();
844
+ listeners.forEach((listener) => listener(next));
845
+ }
846
+ function normalizeConversations(items) {
847
+ const active = activeConversationId;
848
+ const zeroed = active === null ? items : items.map(
849
+ (item) => item.conversation.id === active && item.unreadCount !== 0 ? { ...item, unreadCount: 0 } : item
850
+ );
851
+ return [...zeroed].sort((a, b) => a.sortAt < b.sortAt ? 1 : a.sortAt > b.sortAt ? -1 : 0);
852
+ }
853
+ function threadFor(id) {
854
+ return threads.get(id) ?? {
855
+ conversationId: id,
856
+ messages: [],
857
+ hasMoreOlder: false,
858
+ latestAt: null
859
+ };
860
+ }
861
+ function writeThread(thread) {
862
+ const next = new Map(threads);
863
+ const latest = thread.messages.at(-1);
864
+ next.set(thread.conversationId, {
865
+ ...thread,
866
+ latestAt: latest?.createdAt ?? thread.latestAt
867
+ });
868
+ threads = next;
869
+ }
870
+ const store = {
871
+ snapshot,
872
+ subscribe(listener) {
873
+ listeners.add(listener);
874
+ return () => {
875
+ listeners.delete(listener);
876
+ };
877
+ },
878
+ setConversations(items, hasMore) {
879
+ conversations = normalizeConversations(items);
880
+ hasMoreConversations = hasMore;
881
+ hasLoadedConversations = true;
882
+ emit();
883
+ },
884
+ appendConversations(items, hasMore) {
885
+ const byId = new Map(conversations.map((item) => [item.conversation.id, item]));
886
+ items.forEach((item) => byId.set(item.conversation.id, item));
887
+ conversations = normalizeConversations([...byId.values()]);
888
+ hasMoreConversations = hasMore;
889
+ emit();
890
+ },
891
+ upsertConversation(item) {
892
+ const byId = new Map(conversations.map((entry) => [entry.conversation.id, entry]));
893
+ byId.set(item.conversation.id, item);
894
+ conversations = normalizeConversations([...byId.values()]);
895
+ emit();
896
+ },
897
+ removeConversation(id) {
898
+ conversations = conversations.filter((item) => item.conversation.id !== id);
899
+ const next = new Map(threads);
900
+ next.delete(id);
901
+ threads = next;
902
+ emit();
903
+ },
904
+ setActiveConversation(id) {
905
+ activeConversationId = id;
906
+ conversations = normalizeConversations(conversations);
907
+ emit();
908
+ },
909
+ setThread(id, messages, args = {}) {
910
+ writeThread({
911
+ conversationId: id,
912
+ messages: [...messages].sort(compare),
913
+ hasMoreOlder: args.hasMoreOlder ?? false,
914
+ latestAt: null
915
+ });
916
+ emit();
917
+ },
918
+ prependOlder(id, messages, hasMoreOlder) {
919
+ const thread = threadFor(id);
920
+ const known = new Set(thread.messages.map((message) => message.id));
921
+ const fresh = messages.filter((message) => !known.has(message.id));
922
+ writeThread({
923
+ ...thread,
924
+ messages: [...fresh, ...thread.messages].sort(compare),
925
+ hasMoreOlder
926
+ });
927
+ emit();
928
+ },
929
+ ingest(message) {
930
+ const thread = threadFor(message.conversationId);
931
+ const index = thread.messages.findIndex((held2) => sameMessage(held2, message));
932
+ if (index === -1) {
933
+ writeThread({ ...thread, messages: insertOrdered(thread.messages, message) });
934
+ emit();
935
+ return "added";
936
+ }
937
+ const held = thread.messages[index];
938
+ if (held === void 0) return "dropped-duplicate";
939
+ const heldIsOptimistic = held.deliveryState === "sending" || held.deliveryState === "failed";
940
+ if (!heldIsOptimistic && timeOf(message) < timeOf(held)) return "dropped-stale";
941
+ if (!heldIsOptimistic && held.id === message.id && timeOf(message) === timeOf(held)) {
942
+ return "dropped-duplicate";
943
+ }
944
+ const merged = {
945
+ ...held,
946
+ ...message,
947
+ // Never lose the client key — it is what future echoes match on.
948
+ clientMessageId: message.clientMessageId ?? held.clientMessageId
949
+ };
950
+ const messages = [...thread.messages];
951
+ messages[index] = merged;
952
+ writeThread({ ...thread, messages: messages.sort(compare) });
953
+ emit();
954
+ return "merged";
955
+ },
956
+ ingestMany(messages) {
957
+ messages.forEach((message) => {
958
+ store.ingest(message);
959
+ });
960
+ },
961
+ removeMessage(conversationId, messageId) {
962
+ const thread = threadFor(conversationId);
963
+ writeThread({
964
+ ...thread,
965
+ messages: thread.messages.filter((message) => message.id !== messageId)
966
+ });
967
+ emit();
968
+ },
969
+ markConversationRead(id) {
970
+ conversations = conversations.map(
971
+ (item) => item.conversation.id === id ? { ...item, unreadCount: 0 } : item
972
+ );
973
+ emit();
974
+ },
975
+ setUnreadCount(id, count) {
976
+ conversations = normalizeConversations(
977
+ conversations.map(
978
+ (item) => item.conversation.id === id ? { ...item, unreadCount: Math.max(0, count) } : item
979
+ )
980
+ );
981
+ emit();
982
+ }
983
+ };
984
+ return store;
985
+ }
986
+ function optimisticMessage(args) {
987
+ const at = new Date(args.now?.() ?? Date.now()).toISOString();
988
+ return {
989
+ // A temporary id that can never collide with a uuid from the database.
990
+ id: `optimistic:${args.clientMessageId}`,
991
+ conversationId: args.conversationId,
992
+ senderId: args.senderId,
993
+ organizationId: args.organizationId,
994
+ content: args.content,
995
+ kind: args.kind ?? "text",
996
+ deliveryState: "sending",
997
+ replyToId: args.replyToId ?? null,
998
+ clientMessageId: args.clientMessageId,
999
+ createdAt: at,
1000
+ editedAt: null,
1001
+ deletedAt: null,
1002
+ deletedForEveryone: false,
1003
+ action: args.action ?? null,
1004
+ attachments: args.attachments ?? [],
1005
+ references: args.references ?? [],
1006
+ metadata: {}
1007
+ };
1008
+ }
1009
+
1010
+ // src/core/engine.ts
1011
+ function createMessagingEngine(options) {
1012
+ const { repository, manager, identity } = options;
1013
+ const store = createMessagingStore();
1014
+ const conversationPageSize = options.conversationPageSize ?? 30;
1015
+ const messagePageSize = options.messagePageSize ?? 50;
1016
+ const openChannels = /* @__PURE__ */ new Map();
1017
+ let inboxChannel = null;
1018
+ let conversationCursor = null;
1019
+ let disposed = false;
1020
+ function report(event) {
1021
+ options.onDiagnostic?.(event);
1022
+ }
1023
+ function reportError(error, operation) {
1024
+ const normalized = normalizeMessagingError(error, operation);
1025
+ report({
1026
+ // A missing session is a NORMAL lifecycle moment, not a red error. This
1027
+ // one line is the cure for "909 captured errors in 0.6s".
1028
+ level: normalized.code === "session-unavailable" ? "warn" : "error",
1029
+ message: normalized.message,
1030
+ remedy: normalized.remedy
1031
+ });
1032
+ }
1033
+ const outbox = createOutbox({
1034
+ ...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
1035
+ send: (draft, clientMessageId) => repository.insertMessage(draft, clientMessageId),
1036
+ onSent: (entry, message) => {
1037
+ store.ingest(message);
1038
+ broadcastMessage(message);
1039
+ void refreshConversationRow(message.conversationId);
1040
+ void entry;
1041
+ },
1042
+ onChange: (entries) => {
1043
+ entries.forEach((entry) => {
1044
+ store.ingest(
1045
+ optimisticMessage({
1046
+ conversationId: entry.draft.conversationId,
1047
+ senderId: identity.userId,
1048
+ organizationId: identity.organizationId,
1049
+ content: entry.draft.content,
1050
+ clientMessageId: entry.clientMessageId,
1051
+ ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
1052
+ ...entry.draft.replyToId !== void 0 ? { replyToId: entry.draft.replyToId } : {},
1053
+ ...entry.draft.action !== void 0 ? { action: entry.draft.action } : {},
1054
+ ...entry.draft.attachments !== void 0 ? { attachments: [...entry.draft.attachments] } : {},
1055
+ ...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {}
1056
+ })
1057
+ );
1058
+ });
1059
+ },
1060
+ onDiagnostic: (message) => report({ level: "warn", message })
1061
+ });
1062
+ function broadcastMessage(message) {
1063
+ openChannels.get(message.conversationId)?.send(MESSAGING_EVENTS.message, message);
1064
+ }
1065
+ async function refreshConversationRow(id) {
1066
+ try {
1067
+ const page = await repository.listConversations({ limit: conversationPageSize });
1068
+ const row = page.items.find((item) => item.conversation.id === id);
1069
+ if (row !== void 0) store.upsertConversation(row);
1070
+ } catch (error) {
1071
+ reportError(error, "refreshConversationRow");
1072
+ }
1073
+ }
1074
+ async function reloadInbox() {
1075
+ const page = await repository.listConversations({ limit: conversationPageSize });
1076
+ conversationCursor = page.nextCursor;
1077
+ store.setConversations(page.items, page.hasMore);
1078
+ }
1079
+ async function backfillConversation(id) {
1080
+ const thread = store.snapshot().threads.get(id);
1081
+ const since = thread?.latestAt ?? null;
1082
+ try {
1083
+ if (since === null) {
1084
+ const page = await repository.listMessages(id, { limit: messagePageSize });
1085
+ store.setThread(id, page.items, { hasMoreOlder: page.hasMore });
1086
+ return;
1087
+ }
1088
+ const missed = await repository.messagesSince(id, since);
1089
+ store.ingestMany(missed);
1090
+ if (missed.length > 0) {
1091
+ report({
1092
+ level: "info",
1093
+ message: `Recovered ${missed.length} message(s) missed while disconnected.`
1094
+ });
1095
+ }
1096
+ } catch (error) {
1097
+ reportError(error, "backfillConversation");
1098
+ }
1099
+ }
1100
+ const engine = {
1101
+ store,
1102
+ outbox,
1103
+ identity,
1104
+ async start() {
1105
+ await reloadInbox();
1106
+ if (disposed || inboxChannel !== null) return;
1107
+ inboxChannel = manager.open({
1108
+ topic: inboxTopic(identity.userId),
1109
+ postgresChanges: [
1110
+ {
1111
+ event: "INSERT",
1112
+ schema: "communication",
1113
+ table: "dm_messages",
1114
+ rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
1115
+ onChange: ({ row }) => {
1116
+ if (row === null) return;
1117
+ const message = projectMessage(row, identity.organizationId);
1118
+ const known = store.snapshot().conversations.some((item) => item.conversation.id === message.conversationId);
1119
+ if (!known) {
1120
+ void reloadInbox();
1121
+ return;
1122
+ }
1123
+ store.ingest(message);
1124
+ if (message.senderId !== identity.userId) {
1125
+ options.onIncoming?.(message);
1126
+ }
1127
+ void refreshConversationRow(message.conversationId);
1128
+ }
1129
+ },
1130
+ {
1131
+ event: "*",
1132
+ schema: "communication",
1133
+ table: "dm_conversation_participants",
1134
+ filter: `user_id=eq.${identity.userId}`,
1135
+ rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
1136
+ onChange: () => {
1137
+ void reloadInbox();
1138
+ }
1139
+ }
1140
+ ],
1141
+ // THE BACKFILL DOOR for the inbox.
1142
+ onBackfill: () => {
1143
+ void reloadInbox().catch((error) => reportError(error, "inboxBackfill"));
1144
+ outbox.flush();
1145
+ }
1146
+ });
1147
+ },
1148
+ async loadMoreConversations() {
1149
+ if (conversationCursor === null) return;
1150
+ try {
1151
+ const page = await repository.listConversations({
1152
+ limit: conversationPageSize,
1153
+ cursor: conversationCursor
1154
+ });
1155
+ conversationCursor = page.nextCursor;
1156
+ store.appendConversations(page.items, page.hasMore);
1157
+ } catch (error) {
1158
+ reportError(error, "loadMoreConversations");
1159
+ }
1160
+ },
1161
+ async openConversation(id) {
1162
+ store.setActiveConversation(id);
1163
+ try {
1164
+ const page = await repository.listMessages(id, { limit: messagePageSize });
1165
+ store.setThread(id, page.items, { hasMoreOlder: page.hasMore });
1166
+ } catch (error) {
1167
+ reportError(error, "openConversation");
1168
+ }
1169
+ if (openChannels.has(id) || disposed) return;
1170
+ const handle = manager.open({
1171
+ topic: conversationTopic(id),
1172
+ postgresChanges: [
1173
+ {
1174
+ event: "*",
1175
+ schema: "communication",
1176
+ table: "dm_messages",
1177
+ filter: `conversation_id=eq.${id}`,
1178
+ rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
1179
+ fingerprint: (row) => typeof row["content"] === "string" ? row["content"] : void 0,
1180
+ updatedAtField: "updated_at",
1181
+ updatedByField: "updated_by",
1182
+ onChange: ({ row }) => {
1183
+ if (row === null) return;
1184
+ store.ingest(projectMessage(row, identity.organizationId));
1185
+ }
1186
+ }
1187
+ ],
1188
+ broadcast: [
1189
+ {
1190
+ event: MESSAGING_EVENTS.message,
1191
+ onMessage: ({ data }) => {
1192
+ if (typeof data !== "object" || data === null) return;
1193
+ store.ingest(data);
1194
+ }
1195
+ }
1196
+ ],
1197
+ onBackfill: () => {
1198
+ void backfillConversation(id);
1199
+ outbox.flush();
1200
+ }
1201
+ });
1202
+ openChannels.set(id, handle);
1203
+ },
1204
+ closeConversation(id) {
1205
+ openChannels.get(id)?.close();
1206
+ openChannels.delete(id);
1207
+ if (store.snapshot().activeConversationId === id) {
1208
+ store.setActiveConversation(null);
1209
+ }
1210
+ },
1211
+ async loadOlderMessages(id) {
1212
+ const thread = store.snapshot().threads.get(id);
1213
+ const oldest = thread?.messages[0];
1214
+ if (thread === void 0 || oldest === void 0 || !thread.hasMoreOlder) return;
1215
+ try {
1216
+ const page = await repository.listMessages(id, {
1217
+ limit: messagePageSize,
1218
+ cursor: { beforeCreatedAt: oldest.createdAt, beforeMessageId: oldest.id }
1219
+ });
1220
+ store.prependOlder(id, page.items, page.hasMore);
1221
+ } catch (error) {
1222
+ reportError(error, "loadOlderMessages");
1223
+ }
1224
+ },
1225
+ send(draft) {
1226
+ if (draft.content.trim().length === 0 && (draft.attachments ?? []).length === 0) {
1227
+ throw new MessagingError(
1228
+ "misconfigured",
1229
+ "send: empty draft",
1230
+ "A message needs text or at least one attachment. The composer disables Send for an empty draft rather than queueing nothing."
1231
+ );
1232
+ }
1233
+ return outbox.enqueue(draft);
1234
+ },
1235
+ retry: (entryId) => outbox.retry(entryId),
1236
+ discard: (entryId) => outbox.discard(entryId),
1237
+ async editMessage(id, conversationId, content) {
1238
+ try {
1239
+ const message = await repository.editMessage(id, content);
1240
+ store.ingest(message);
1241
+ broadcastMessage(message);
1242
+ } catch (error) {
1243
+ reportError(error, "editMessage");
1244
+ throw normalizeMessagingError(error, "editMessage");
1245
+ }
1246
+ void conversationId;
1247
+ },
1248
+ async deleteMessage(id, conversationId, forEveryone) {
1249
+ try {
1250
+ await repository.deleteMessage(id, forEveryone);
1251
+ store.removeMessage(conversationId, id);
1252
+ } catch (error) {
1253
+ reportError(error, "deleteMessage");
1254
+ throw normalizeMessagingError(error, "deleteMessage");
1255
+ }
1256
+ },
1257
+ async markRead(id) {
1258
+ store.markConversationRead(id);
1259
+ try {
1260
+ await repository.markRead(id);
1261
+ } catch (error) {
1262
+ reportError(error, "markRead");
1263
+ }
1264
+ },
1265
+ async startDirectConversation(otherUserId) {
1266
+ const id = await repository.getOrCreateDirectConversation(otherUserId);
1267
+ await reloadInbox();
1268
+ return id;
1269
+ },
1270
+ dispose() {
1271
+ disposed = true;
1272
+ outbox.dispose();
1273
+ openChannels.forEach((handle) => handle.close());
1274
+ openChannels.clear();
1275
+ inboxChannel?.close();
1276
+ inboxChannel = null;
1277
+ }
1278
+ };
1279
+ return engine;
1280
+ }
1281
+ function messagingClientId() {
1282
+ return clientSessionId();
1283
+ }
1284
+
1285
+ // src/core/cache.ts
1286
+ function createReadCache(options) {
1287
+ const now = options.now ?? (() => Date.now());
1288
+ const maxEntries = options.maxEntries ?? 500;
1289
+ const resolved = /* @__PURE__ */ new Map();
1290
+ const inFlight = /* @__PURE__ */ new Map();
1291
+ function evictIfNeeded() {
1292
+ while (resolved.size > maxEntries) {
1293
+ const oldest = resolved.keys().next();
1294
+ if (oldest.done === true) return;
1295
+ resolved.delete(oldest.value);
1296
+ }
1297
+ }
1298
+ return {
1299
+ read(key, load) {
1300
+ const hit = resolved.get(key);
1301
+ if (hit !== void 0 && hit.expiresAt > now()) {
1302
+ return Promise.resolve(hit.value);
1303
+ }
1304
+ if (hit !== void 0) resolved.delete(key);
1305
+ const pending = inFlight.get(key);
1306
+ if (pending !== void 0) return pending;
1307
+ const started = load().then((value) => {
1308
+ resolved.set(key, { value, expiresAt: now() + options.ttlMs });
1309
+ evictIfNeeded();
1310
+ return value;
1311
+ }).finally(() => {
1312
+ inFlight.delete(key);
1313
+ });
1314
+ inFlight.set(key, started);
1315
+ return started;
1316
+ },
1317
+ invalidate(key) {
1318
+ resolved.delete(key);
1319
+ inFlight.delete(key);
1320
+ },
1321
+ clear() {
1322
+ resolved.clear();
1323
+ inFlight.clear();
1324
+ },
1325
+ size() {
1326
+ return resolved.size;
1327
+ }
1328
+ };
1329
+ }
1330
+
1331
+ // src/core/repository.ts
1332
+ var MESSAGING_SCHEMA = "communication";
1333
+ var TABLES = {
1334
+ conversations: "dm_conversations",
1335
+ participants: "dm_conversation_participants",
1336
+ messages: "dm_messages"
1337
+ };
1338
+ var RPCS = {
1339
+ /** Atomic direct-conversation creation. Advisory-locks the unordered pair. */
1340
+ getOrCreateDirect: "dm_get_or_create_direct_conversation",
1341
+ /** Conversation list + participants + last message + unread, keyset paged. */
1342
+ conversationsWithDetails: "get_dm_conversations_with_details",
1343
+ unreadCount: "get_dm_unread_count",
1344
+ userInfo: "get_dm_user_info",
1345
+ isParticipant: "is_dm_participant"
1346
+ };
1347
+ var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
1348
+ var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1349
+ function requireOrg(organizationId, operation) {
1350
+ if (typeof organizationId !== "string" || organizationId.length === 0) {
1351
+ throw new MessagingError(
1352
+ "misconfigured",
1353
+ `${operation}: no organization_id`,
1354
+ "Every conversation and message write carries an explicit organization_id (R5). Pass a real org on <MessagingProvider>; the package refuses the write rather than letting an unscoped row reach the database."
1355
+ );
1356
+ }
1357
+ return organizationId;
1358
+ }
1359
+ function createMessagingRepository(options) {
1360
+ const { client, identity } = options;
1361
+ const org = requireOrg(identity.organizationId, "createMessagingRepository");
1362
+ const userCache = createReadCache({
1363
+ ttlMs: options.userTtlMs ?? 5 * 6e4,
1364
+ ...options.now !== void 0 ? { now: options.now } : {}
1365
+ });
1366
+ const db = () => client.schema(MESSAGING_SCHEMA);
1367
+ async function withSessionRetry(operation, run) {
1368
+ try {
1369
+ return await run();
1370
+ } catch (error) {
1371
+ const normalized = normalizeMessagingError(error, operation);
1372
+ if (normalized.code !== "session-unavailable" || options.resolveSession === void 0) {
1373
+ throw normalized;
1374
+ }
1375
+ await options.resolveSession();
1376
+ try {
1377
+ return await run();
1378
+ } catch (retryError) {
1379
+ throw normalizeMessagingError(retryError, operation);
1380
+ }
1381
+ }
1382
+ }
1383
+ async function rpc(fn, args, operation) {
1384
+ const { data, error } = await db().rpc(fn, args);
1385
+ if (error !== null) throw normalizeMessagingError(error, operation);
1386
+ return data;
1387
+ }
1388
+ async function getUsers(userIds) {
1389
+ const unique = [...new Set(userIds)];
1390
+ const found = /* @__PURE__ */ new Map();
1391
+ const results = await Promise.all(
1392
+ unique.map(async (id) => {
1393
+ try {
1394
+ return await repository.getUser(id);
1395
+ } catch {
1396
+ return null;
1397
+ }
1398
+ })
1399
+ );
1400
+ results.forEach((summary) => {
1401
+ if (summary !== null) found.set(summary.userId, summary);
1402
+ });
1403
+ return found;
1404
+ }
1405
+ const repository = {
1406
+ identity,
1407
+ async listConversations(args = {}) {
1408
+ const limit = args.limit ?? 30;
1409
+ const operation = "listConversations";
1410
+ const rows = await withSessionRetry(
1411
+ operation,
1412
+ () => rpc(
1413
+ RPCS.conversationsWithDetails,
1414
+ {
1415
+ p_user_id: identity.userId,
1416
+ p_limit: limit + 1,
1417
+ p_before_sort_at: args.cursor?.beforeSortAt ?? null,
1418
+ p_before_conversation_id: args.cursor?.beforeConversationId ?? null
1419
+ },
1420
+ operation
1421
+ )
1422
+ );
1423
+ if (rows !== null && !Array.isArray(rows)) {
1424
+ throw invalidResponse(operation, `${RPCS.conversationsWithDetails} did not return rows`);
1425
+ }
1426
+ const projected = (rows ?? []).map(
1427
+ (row) => projectConversationSummary(row, identity.userId, org)
1428
+ );
1429
+ const hasMore = projected.length > limit;
1430
+ const items = hasMore ? projected.slice(0, limit) : projected;
1431
+ const last = items.at(-1);
1432
+ return {
1433
+ items,
1434
+ hasMore,
1435
+ nextCursor: hasMore && last !== void 0 ? { beforeSortAt: last.sortAt, beforeConversationId: last.conversation.id } : null
1436
+ };
1437
+ },
1438
+ async getConversation(id) {
1439
+ const operation = "getConversation";
1440
+ const { data, error } = await withSessionRetry(
1441
+ operation,
1442
+ async () => db().from(TABLES.conversations).select(CONVERSATION_COLUMNS).eq("id", id).single()
1443
+ );
1444
+ if (error !== null) throw normalizeMessagingError(error, operation);
1445
+ if (data === null) throw invalidResponse(operation, `conversation ${id} returned no row`);
1446
+ const summary = projectConversationSummary(
1447
+ {
1448
+ conversation_id: data["id"],
1449
+ conversation_type: data["type"],
1450
+ group_name: data["group_name"],
1451
+ group_image_url: data["group_image_url"],
1452
+ conversation_created_at: data["created_at"],
1453
+ conversation_updated_at: data["updated_at"],
1454
+ organization_id: data["organization_id"],
1455
+ created_by: data["created_by"],
1456
+ metadata: data["metadata"],
1457
+ participants: [],
1458
+ unread_count: 0
1459
+ },
1460
+ identity.userId,
1461
+ org
1462
+ );
1463
+ return summary.conversation;
1464
+ },
1465
+ async listMessages(conversationId, args = {}) {
1466
+ const limit = args.limit ?? 50;
1467
+ const operation = "listMessages";
1468
+ const rows = await withSessionRetry(operation, async () => {
1469
+ let query = db().from(TABLES.messages).select(MESSAGE_COLUMNS).eq("conversation_id", conversationId);
1470
+ const cursor = args.cursor;
1471
+ if (cursor != null) {
1472
+ query = query.or(
1473
+ `created_at.lt.${cursor.beforeCreatedAt},and(created_at.eq.${cursor.beforeCreatedAt},id.lt.${cursor.beforeMessageId})`
1474
+ );
1475
+ }
1476
+ const { data, error } = await query.order("created_at", { ascending: false }).order("id", { ascending: false }).limit(limit + 1);
1477
+ if (error !== null) throw normalizeMessagingError(error, operation);
1478
+ return data ?? [];
1479
+ });
1480
+ const hasMore = rows.length > limit;
1481
+ const page = hasMore ? rows.slice(0, limit) : rows;
1482
+ const oldest = page.at(-1);
1483
+ const items = page.map((row) => projectMessage(row, org)).reverse();
1484
+ return {
1485
+ items,
1486
+ hasMore,
1487
+ nextCursor: hasMore && oldest !== void 0 ? {
1488
+ beforeCreatedAt: String(oldest["created_at"]),
1489
+ beforeMessageId: String(oldest["id"])
1490
+ } : null
1491
+ };
1492
+ },
1493
+ async messagesSince(conversationId, since) {
1494
+ const operation = "messagesSince";
1495
+ const rows = await withSessionRetry(operation, async () => {
1496
+ const { data, error } = await db().from(TABLES.messages).select(MESSAGE_COLUMNS).eq("conversation_id", conversationId).gt("created_at", since).order("created_at", { ascending: true }).limit(500);
1497
+ if (error !== null) throw normalizeMessagingError(error, operation);
1498
+ return data ?? [];
1499
+ });
1500
+ return rows.map((row) => projectMessage(row, org));
1501
+ },
1502
+ async getOrCreateDirectConversation(otherUserId) {
1503
+ const operation = "getOrCreateDirectConversation";
1504
+ const id = await withSessionRetry(
1505
+ operation,
1506
+ () => rpc(
1507
+ RPCS.getOrCreateDirect,
1508
+ {
1509
+ // The RPC's own guard requires an `authenticated` caller to pass
1510
+ // THEMSELVES as user1; passing them in the other slot is a denial,
1511
+ // not a preference.
1512
+ p_user1_id: identity.userId,
1513
+ p_user2_id: otherUserId,
1514
+ p_organization_id: org
1515
+ },
1516
+ operation
1517
+ )
1518
+ );
1519
+ if (typeof id !== "string" || id.length === 0) {
1520
+ throw invalidResponse(operation, `${RPCS.getOrCreateDirect} returned no conversation id`);
1521
+ }
1522
+ return id;
1523
+ },
1524
+ async createGroupConversation({ name, memberIds }) {
1525
+ const operation = "createGroupConversation";
1526
+ const { data, error } = await withSessionRetry(
1527
+ operation,
1528
+ async () => db().from(TABLES.conversations).insert({
1529
+ type: "group",
1530
+ group_name: name,
1531
+ organization_id: org,
1532
+ created_by: identity.userId
1533
+ }).select("id").single()
1534
+ );
1535
+ if (error !== null) throw normalizeMessagingError(error, operation);
1536
+ const conversationId = data?.["id"];
1537
+ if (typeof conversationId !== "string") {
1538
+ throw invalidResponse(operation, "insert returned no conversation id");
1539
+ }
1540
+ const members = [.../* @__PURE__ */ new Set([identity.userId, ...memberIds])];
1541
+ const { error: memberError } = await db().from(TABLES.participants).insert(
1542
+ members.map((userId) => ({
1543
+ conversation_id: conversationId,
1544
+ user_id: userId,
1545
+ role: userId === identity.userId ? "owner" : "member",
1546
+ organization_id: org,
1547
+ created_by: identity.userId
1548
+ }))
1549
+ );
1550
+ if (memberError !== null) throw normalizeMessagingError(memberError, operation);
1551
+ return conversationId;
1552
+ },
1553
+ async insertMessage(draft, clientMessageId) {
1554
+ const operation = "insertMessage";
1555
+ const { data, error } = await db().from(TABLES.messages).insert({
1556
+ conversation_id: draft.conversationId,
1557
+ sender_id: identity.userId,
1558
+ organization_id: org,
1559
+ created_by: identity.userId,
1560
+ content: draft.content,
1561
+ message_type: draft.kind ?? "text",
1562
+ status: "sent",
1563
+ reply_to_id: draft.replyToId ?? null,
1564
+ // THE IDEMPOTENCY KEY. It is what makes a retried send exactly-once
1565
+ // and what lets a receiver collapse the optimistic bubble with the
1566
+ // confirmed row instead of showing the message twice.
1567
+ client_message_id: clientMessageId,
1568
+ action_data: draft.action ?? null,
1569
+ metadata: {
1570
+ ...draft.metadata ?? {},
1571
+ ...draft.attachments !== void 0 && draft.attachments.length > 0 ? { attachments: draft.attachments } : {},
1572
+ ...draft.references !== void 0 && draft.references.length > 0 ? { references: draft.references } : {}
1573
+ }
1574
+ }).select(MESSAGE_COLUMNS).single();
1575
+ if (error !== null) throw normalizeMessagingError(error, operation);
1576
+ if (data === null) throw invalidResponse(operation, "insert returned no row");
1577
+ return projectMessage(data, org);
1578
+ },
1579
+ async editMessage(id, content) {
1580
+ const operation = "editMessage";
1581
+ const { data, error } = await db().from(TABLES.messages).update({
1582
+ content,
1583
+ edited_at: (/* @__PURE__ */ new Date()).toISOString(),
1584
+ updated_by: identity.userId
1585
+ }).eq("id", id).select(MESSAGE_COLUMNS).single();
1586
+ if (error !== null) throw normalizeMessagingError(error, operation);
1587
+ if (data === null) throw invalidResponse(operation, "update returned no row");
1588
+ return projectMessage(data, org);
1589
+ },
1590
+ async deleteMessage(id, forEveryone) {
1591
+ const operation = "deleteMessage";
1592
+ const { error } = await db().from(TABLES.messages).update({
1593
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
1594
+ deleted_for_everyone: forEveryone,
1595
+ updated_by: identity.userId
1596
+ }).eq("id", id);
1597
+ if (error !== null) throw normalizeMessagingError(error, operation);
1598
+ },
1599
+ async markRead(conversationId, at) {
1600
+ const operation = "markRead";
1601
+ const { error } = await db().from(TABLES.participants).update({ last_read_at: at ?? (/* @__PURE__ */ new Date()).toISOString(), updated_by: identity.userId }).eq("conversation_id", conversationId).eq("user_id", identity.userId);
1602
+ if (error !== null) throw normalizeMessagingError(error, operation);
1603
+ },
1604
+ async setConversationFlags(conversationId, flags) {
1605
+ const operation = "setConversationFlags";
1606
+ const patch = { updated_by: identity.userId };
1607
+ if (flags.isMuted !== void 0) patch["is_muted"] = flags.isMuted;
1608
+ if (flags.isArchived !== void 0) patch["is_archived"] = flags.isArchived;
1609
+ const { error } = await db().from(TABLES.participants).update(patch).eq("conversation_id", conversationId).eq("user_id", identity.userId);
1610
+ if (error !== null) throw normalizeMessagingError(error, operation);
1611
+ },
1612
+ async addMembers(conversationId, memberIds) {
1613
+ const operation = "addMembers";
1614
+ const { error } = await db().from(TABLES.participants).insert(
1615
+ memberIds.map((userId) => ({
1616
+ conversation_id: conversationId,
1617
+ user_id: userId,
1618
+ role: "member",
1619
+ organization_id: org,
1620
+ created_by: identity.userId
1621
+ }))
1622
+ );
1623
+ if (error !== null) throw normalizeMessagingError(error, operation);
1624
+ },
1625
+ async removeMember(conversationId, memberId) {
1626
+ const operation = "removeMember";
1627
+ const { error } = await db().from(TABLES.participants).update({ deleted_at: (/* @__PURE__ */ new Date()).toISOString(), updated_by: identity.userId }).eq("conversation_id", conversationId).eq("user_id", memberId);
1628
+ if (error !== null) throw normalizeMessagingError(error, operation);
1629
+ },
1630
+ async setMemberRole(conversationId, memberId, role) {
1631
+ const operation = "setMemberRole";
1632
+ const { error } = await db().from(TABLES.participants).update({ role, updated_by: identity.userId }).eq("conversation_id", conversationId).eq("user_id", memberId);
1633
+ if (error !== null) throw normalizeMessagingError(error, operation);
1634
+ },
1635
+ getUser(userId) {
1636
+ const operation = "getUser";
1637
+ return userCache.read(userId, async () => {
1638
+ const rows = await withSessionRetry(
1639
+ operation,
1640
+ () => rpc(
1641
+ RPCS.userInfo,
1642
+ { p_user_id: userId },
1643
+ operation
1644
+ )
1645
+ );
1646
+ const row = Array.isArray(rows) ? rows[0] : null;
1647
+ return row === void 0 || row === null ? null : projectUserSummary(row);
1648
+ });
1649
+ },
1650
+ getUsers,
1651
+ async searchMessages({ query, conversationId = null, limit = 50 }) {
1652
+ const operation = "searchMessages";
1653
+ const trimmed = query.trim();
1654
+ if (trimmed.length === 0) return [];
1655
+ const rows = await withSessionRetry(operation, async () => {
1656
+ let base = db().from(TABLES.messages).select(MESSAGE_COLUMNS).is("deleted_at", null);
1657
+ if (conversationId !== null) base = base.eq("conversation_id", conversationId);
1658
+ const safe = trimmed.replace(/[%,()*]/g, " ").trim();
1659
+ const { data, error } = await base.or(`content.ilike.*${safe}*`).order("created_at", { ascending: false }).limit(limit);
1660
+ if (error !== null) throw normalizeMessagingError(error, operation);
1661
+ return data ?? [];
1662
+ });
1663
+ return rows.map((row) => projectMessage(row, org));
1664
+ },
1665
+ invalidateUser(userId) {
1666
+ userCache.invalidate(userId);
1667
+ }
1668
+ };
1669
+ return repository;
1670
+ }
1671
+
1672
+ // src/react/provider.tsx
1673
+ import { jsx } from "react/jsx-runtime";
1674
+ var MessagingContext = createContext(null);
1675
+ function defaultDiagnostics(event) {
1676
+ const line = `[@ai-matrx/messaging] ${event.message}${event.remedy !== void 0 ? `
1677
+ \u2192 ${event.remedy}` : ""}`;
1678
+ if (event.level === "error") console.error(line);
1679
+ else if (event.level === "warn") console.warn(line);
1680
+ else console.info(line);
1681
+ }
1682
+ function MessagingProvider(props) {
1683
+ const { client, userId, organizationId } = props;
1684
+ const ready = client != null && typeof userId === "string" && userId.length > 0 && typeof organizationId === "string" && organizationId.length > 0;
1685
+ return /* @__PURE__ */ jsx(RealtimeProvider, { client: ready ? client : null, actorId: userId ?? void 0, children: /* @__PURE__ */ jsx(MessagingRuntime, { ...props }) });
1686
+ }
1687
+ function MessagingRuntime(props) {
1688
+ const { client, userId, organizationId, children } = props;
1689
+ const manager = useRealtimeManager();
1690
+ const diagnosticRef = useRef(props.onDiagnostic);
1691
+ diagnosticRef.current = props.onDiagnostic;
1692
+ const incomingRef = useRef(props.onIncomingMessage);
1693
+ incomingRef.current = props.onIncomingMessage;
1694
+ const referenceRef = useRef(props.onOpenReference);
1695
+ referenceRef.current = props.onOpenReference;
1696
+ const actionRegistry = useMemo(() => createActionRegistry(), []);
1697
+ const handlers = props.actions;
1698
+ useEffect(() => {
1699
+ (handlers ?? []).forEach((handler) => {
1700
+ actionRegistry.register(handler);
1701
+ });
1702
+ }, [actionRegistry, handlers]);
1703
+ const ready = client != null && manager !== null && typeof userId === "string" && userId.length > 0 && typeof organizationId === "string" && organizationId.length > 0;
1704
+ const engine = useMemo(() => {
1705
+ if (!ready || manager === null) return null;
1706
+ const identity = {
1707
+ userId,
1708
+ organizationId
1709
+ };
1710
+ const report = (event) => {
1711
+ (diagnosticRef.current ?? defaultDiagnostics)(event);
1712
+ };
1713
+ const repository = createMessagingRepository({
1714
+ client,
1715
+ identity,
1716
+ ...props.resolveSession !== void 0 ? { resolveSession: props.resolveSession } : {}
1717
+ });
1718
+ return createMessagingEngine({
1719
+ repository,
1720
+ manager,
1721
+ identity,
1722
+ outboxStorage: props.outboxStorage ?? createWebOutboxStorage({
1723
+ key: `ai-matrx.messaging.outbox.${identity.userId}`,
1724
+ onFallback: (message) => report({ level: "warn", message })
1725
+ }),
1726
+ onDiagnostic: report,
1727
+ onIncoming: (message) => incomingRef.current?.(message)
1728
+ });
1729
+ }, [ready, manager, userId, organizationId]);
1730
+ useEffect(() => {
1731
+ if (engine === null) return void 0;
1732
+ void engine.start().catch((error) => {
1733
+ (diagnosticRef.current ?? defaultDiagnostics)({
1734
+ level: "error",
1735
+ message: `Messaging failed to start: ${String(error?.message ?? error)}`,
1736
+ remedy: "Check the signed-in session and organization, then reload the surface."
1737
+ });
1738
+ });
1739
+ return () => {
1740
+ engine.dispose();
1741
+ };
1742
+ }, [engine]);
1743
+ const transport = props.transport;
1744
+ const agents = props.agents;
1745
+ const ai = useMemo(() => {
1746
+ if (transport === void 0 || agents === void 0 || !ready) return null;
1747
+ return createMessagingAi({
1748
+ transport,
1749
+ organizationId,
1750
+ agents
1751
+ });
1752
+ }, [transport, agents, ready, organizationId]);
1753
+ const host = useMemo(() => {
1754
+ if (engine === null) return null;
1755
+ return {
1756
+ engine,
1757
+ actions: actionRegistry,
1758
+ ai,
1759
+ identity: engine.identity,
1760
+ openReference: referenceRef.current ?? null
1761
+ };
1762
+ }, [engine, actionRegistry, ai]);
1763
+ return /* @__PURE__ */ jsx(MessagingContext.Provider, { value: host, children });
1764
+ }
1765
+ function useMessagingHost() {
1766
+ return useContext(MessagingContext);
1767
+ }
1768
+ function useRequiredMessagingHost() {
1769
+ const host = useMessagingHost();
1770
+ if (host === null) {
1771
+ throw new Error(
1772
+ "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} userId={userId} organizationId={orgId}> and render it only once auth has resolved."
1773
+ );
1774
+ }
1775
+ return host;
1776
+ }
1777
+ function useMessagingSnapshot() {
1778
+ const host = useMessagingHost();
1779
+ const store = host?.engine.store ?? null;
1780
+ const empty = useRef(null);
1781
+ return useSyncExternalStore(
1782
+ (listener) => store === null ? () => void 0 : store.subscribe(listener),
1783
+ () => store === null ? empty.current : store.snapshot(),
1784
+ () => store === null ? empty.current : store.snapshot()
1785
+ );
1786
+ }
1787
+
1788
+ // src/react/hooks.ts
1789
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState } from "react";
1790
+ import { useTyping, usePresence } from "@ai-matrx/realtime/react";
1791
+
1792
+ // src/core/format.ts
1793
+ var MINUTE = 6e4;
1794
+ var HOUR = 60 * MINUTE;
1795
+ var DAY = 24 * HOUR;
1796
+ function isSameDay(a, b) {
1797
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1798
+ }
1799
+ function formatConversationTime(isoString, now = Date.now(), locale) {
1800
+ if (isoString === null) return "";
1801
+ const parsed = Date.parse(isoString);
1802
+ if (!Number.isFinite(parsed)) return "";
1803
+ const then = new Date(parsed);
1804
+ const today = new Date(now);
1805
+ if (isSameDay(then, today)) {
1806
+ return then.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit" });
1807
+ }
1808
+ const yesterday = new Date(now - DAY);
1809
+ if (isSameDay(then, yesterday)) return "Yesterday";
1810
+ if (now - parsed < 7 * DAY) return then.toLocaleDateString(locale, { weekday: "short" });
1811
+ return then.toLocaleDateString(locale, { month: "short", day: "numeric" });
1812
+ }
1813
+ function formatMessageTime(isoString, locale) {
1814
+ const parsed = Date.parse(isoString);
1815
+ if (!Number.isFinite(parsed)) return "";
1816
+ return new Date(parsed).toLocaleTimeString(locale, {
1817
+ hour: "numeric",
1818
+ minute: "2-digit"
1819
+ });
1820
+ }
1821
+ function formatDateSeparator(isoString, now = Date.now(), locale) {
1822
+ const parsed = Date.parse(isoString);
1823
+ if (!Number.isFinite(parsed)) return "";
1824
+ const then = new Date(parsed);
1825
+ const today = new Date(now);
1826
+ if (isSameDay(then, today)) return "Today";
1827
+ if (isSameDay(then, new Date(now - DAY))) return "Yesterday";
1828
+ if (then.getFullYear() === today.getFullYear()) {
1829
+ return then.toLocaleDateString(locale, { month: "long", day: "numeric" });
1830
+ }
1831
+ return then.toLocaleDateString(locale, {
1832
+ year: "numeric",
1833
+ month: "long",
1834
+ day: "numeric"
1835
+ });
1836
+ }
1837
+ function getInitials(name) {
1838
+ const parts = name.trim().split(/\s+/).filter(Boolean);
1839
+ if (parts.length === 0) return "?";
1840
+ const first = parts[0]?.[0] ?? "";
1841
+ const last = parts.length > 1 ? parts.at(-1)?.[0] ?? "" : "";
1842
+ return `${first}${last}`.toUpperCase() || "?";
1843
+ }
1844
+ function avatarPaletteIndex(seed, buckets = 8) {
1845
+ let hash = 0;
1846
+ for (let index = 0; index < seed.length; index += 1) {
1847
+ hash = hash * 31 + seed.charCodeAt(index) | 0;
1848
+ }
1849
+ return Math.abs(hash) % buckets;
1850
+ }
1851
+ function groupMessages(messages, args = {}) {
1852
+ const windowMs = args.windowMs ?? 5 * MINUTE;
1853
+ const now = args.now ?? Date.now();
1854
+ const groups = [];
1855
+ let current = null;
1856
+ let previousDay = null;
1857
+ for (const message of messages) {
1858
+ const day = message.createdAt.slice(0, 10);
1859
+ const startsNewDay = day !== previousDay;
1860
+ previousDay = day;
1861
+ const last = current?.messages.at(-1);
1862
+ const withinWindow = last !== void 0 && Math.abs(Date.parse(message.createdAt) - Date.parse(last.createdAt)) <= windowMs;
1863
+ if (current !== null && current.senderId === message.senderId && withinWindow && !startsNewDay) {
1864
+ current.messages.push(message);
1865
+ continue;
1866
+ }
1867
+ if (current !== null) groups.push(current);
1868
+ current = {
1869
+ senderId: message.senderId,
1870
+ messages: [message],
1871
+ dateSeparator: startsNewDay ? formatDateSeparator(message.createdAt, now, args.locale) : null
1872
+ };
1873
+ }
1874
+ if (current !== null) groups.push(current);
1875
+ return groups;
1876
+ }
1877
+ function formatTypists(names) {
1878
+ if (names.length === 0) return null;
1879
+ if (names.length === 1) return `${names[0]} is typing\u2026`;
1880
+ if (names.length === 2) return `${names[0]} and ${names[1]} are typing\u2026`;
1881
+ return `${names.length} people are typing\u2026`;
1882
+ }
1883
+ function participantNames(participants, excluding) {
1884
+ return participants.filter((participant) => participant.userId !== excluding).map((participant) => participant.displayName);
1885
+ }
1886
+ function formatLastSeen(lastSeenMs, now = Date.now()) {
1887
+ if (lastSeenMs === null) return "";
1888
+ const elapsed = now - lastSeenMs;
1889
+ if (elapsed < 2 * MINUTE) return "Active now";
1890
+ if (elapsed < HOUR) return `Active ${Math.round(elapsed / MINUTE)}m ago`;
1891
+ if (elapsed < DAY) return `Active ${Math.round(elapsed / HOUR)}h ago`;
1892
+ return `Active ${Math.round(elapsed / DAY)}d ago`;
1893
+ }
1894
+
1895
+ // src/react/hooks.ts
1896
+ function useConversations() {
1897
+ const host = useMessagingHost();
1898
+ const snapshot = useMessagingSnapshot();
1899
+ const select = useCallback(
1900
+ (id) => {
1901
+ void host?.engine.openConversation(id);
1902
+ void host?.engine.markRead(id);
1903
+ },
1904
+ [host]
1905
+ );
1906
+ return {
1907
+ conversations: snapshot?.conversations ?? [],
1908
+ hasMore: snapshot?.hasMoreConversations ?? false,
1909
+ // "Not loaded yet" is a FACT from the store, never a guess. Showing
1910
+ // "No conversations yet" before the first read is a screen telling a lie.
1911
+ isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations,
1912
+ totalUnreadConversations: snapshot?.totalUnreadConversations ?? 0,
1913
+ loadMore: () => {
1914
+ void host?.engine.loadMoreConversations();
1915
+ },
1916
+ select,
1917
+ activeConversationId: snapshot?.activeConversationId ?? null,
1918
+ startDirect: async (otherUserId) => {
1919
+ if (host === null) {
1920
+ throw new Error("[@ai-matrx/messaging] startDirect called before the host was ready.");
1921
+ }
1922
+ const id = await host.engine.startDirectConversation(otherUserId);
1923
+ select(id);
1924
+ return id;
1925
+ }
1926
+ };
1927
+ }
1928
+ function useConversation(id) {
1929
+ const host = useMessagingHost();
1930
+ const snapshot = useMessagingSnapshot();
1931
+ const [outboxTick, setOutboxTick] = useState(0);
1932
+ useEffect2(() => {
1933
+ if (host === null || id === null) return void 0;
1934
+ void host.engine.openConversation(id);
1935
+ return () => {
1936
+ host.engine.closeConversation(id);
1937
+ };
1938
+ }, [host, id]);
1939
+ useEffect2(() => {
1940
+ const timer = setInterval(() => setOutboxTick((tick) => tick + 1), 1e3);
1941
+ return () => clearInterval(timer);
1942
+ }, []);
1943
+ const thread = id === null ? null : snapshot?.threads.get(id) ?? null;
1944
+ const summary = id === null ? null : snapshot?.conversations.find((item) => item.conversation.id === id) ?? null;
1945
+ const pending = useMemo2(() => {
1946
+ void outboxTick;
1947
+ return id === null ? [] : host?.engine.outbox.pendingFor(id) ?? [];
1948
+ }, [host, id, outboxTick]);
1949
+ return {
1950
+ summary,
1951
+ thread,
1952
+ messages: thread?.messages ?? [],
1953
+ participants: summary?.participants ?? [],
1954
+ isLoading: id !== null && thread === null,
1955
+ hasMoreOlder: thread?.hasMoreOlder ?? false,
1956
+ loadOlder: () => {
1957
+ if (id !== null) void host?.engine.loadOlderMessages(id);
1958
+ },
1959
+ pending,
1960
+ retry: (entryId) => host?.engine.retry(entryId),
1961
+ discard: (entryId) => host?.engine.discard(entryId),
1962
+ edit: async (messageId, content) => {
1963
+ if (host !== null && id !== null) await host.engine.editMessage(messageId, id, content);
1964
+ },
1965
+ remove: async (messageId, forEveryone) => {
1966
+ if (host !== null && id !== null) {
1967
+ await host.engine.deleteMessage(messageId, id, forEveryone);
1968
+ }
1969
+ }
1970
+ };
1971
+ }
1972
+ function useComposer(conversationId) {
1973
+ const host = useMessagingHost();
1974
+ const [drafts, setDrafts] = useState({});
1975
+ const [replyTo, setReplyTo] = useState(null);
1976
+ const value = conversationId === null ? "" : drafts[conversationId] ?? "";
1977
+ const typing = useTyping(
1978
+ {
1979
+ topic: conversationId === null ? "" : conversationTopic(conversationId),
1980
+ identity: { userId: host?.identity.userId ?? "" }
1981
+ },
1982
+ { enabled: conversationId !== null && host !== null }
1983
+ );
1984
+ const setValue = useCallback(
1985
+ (next) => {
1986
+ if (conversationId === null) return;
1987
+ setDrafts((current) => ({ ...current, [conversationId]: next }));
1988
+ },
1989
+ [conversationId]
1990
+ );
1991
+ return {
1992
+ value,
1993
+ setValue,
1994
+ canSend: value.trim().length > 0 && conversationId !== null && host !== null,
1995
+ send: () => {
1996
+ if (host === null || conversationId === null) return;
1997
+ const content = value.trim();
1998
+ if (content.length === 0) return;
1999
+ const draft = {
2000
+ conversationId,
2001
+ content,
2002
+ ...replyTo !== null ? { replyToId: replyTo.id } : {}
2003
+ };
2004
+ host.engine.send(draft);
2005
+ setDrafts((current) => ({ ...current, [conversationId]: "" }));
2006
+ setReplyTo(null);
2007
+ typing.stopTyping();
2008
+ },
2009
+ onKeystroke: typing.onKeystroke,
2010
+ replyTo,
2011
+ setReplyTo
2012
+ };
2013
+ }
2014
+ function useTypists(conversationId, participants) {
2015
+ const host = useMessagingHost();
2016
+ const typing = useTyping(
2017
+ {
2018
+ topic: conversationId === null ? "" : conversationTopic(conversationId),
2019
+ identity: { userId: host?.identity.userId ?? "" }
2020
+ },
2021
+ { enabled: conversationId !== null && host !== null }
2022
+ );
2023
+ const selfId = host?.identity.userId;
2024
+ const others = typing.typists.filter((typist) => {
2025
+ const userId = typist.state["userId"];
2026
+ return typeof userId === "string" && userId !== selfId;
2027
+ });
2028
+ const names = others.map((typist) => {
2029
+ const userId = typist.state["userId"];
2030
+ const match = participants.find((participant) => participant.userId === userId);
2031
+ return match?.displayName ?? "Someone";
2032
+ });
2033
+ return {
2034
+ label: formatTypists(names),
2035
+ userIds: others.flatMap((typist) => {
2036
+ const userId = typist.state["userId"];
2037
+ return typeof userId === "string" ? [userId] : [];
2038
+ })
2039
+ };
2040
+ }
2041
+ function useOnlineUserIds(conversationId) {
2042
+ const host = useMessagingHost();
2043
+ const presence = usePresence(
2044
+ {
2045
+ topic: conversationId === null ? "" : conversationTopic(conversationId),
2046
+ presence: { state: { userId: host?.identity.userId ?? "" } }
2047
+ },
2048
+ { enabled: conversationId !== null && host !== null }
2049
+ );
2050
+ return useMemo2(
2051
+ () => new Set(presence.members.map((member) => member.state.userId).filter(Boolean)),
2052
+ [presence.members]
2053
+ );
2054
+ }
2055
+ function useMessagingAi(conversationId) {
2056
+ const host = useMessagingHost();
2057
+ const conversation = useConversation(conversationId);
2058
+ const [isRunning, setRunning] = useState(false);
2059
+ const [result, setResult] = useState(null);
2060
+ const [error, setError] = useState(null);
2061
+ const abortRef = useRef2(null);
2062
+ useEffect2(
2063
+ () => () => {
2064
+ abortRef.current?.abort();
2065
+ },
2066
+ []
2067
+ );
2068
+ const ai = host?.ai ?? null;
2069
+ return {
2070
+ available: ai?.available() ?? [],
2071
+ isRunning,
2072
+ result,
2073
+ error,
2074
+ clear: () => {
2075
+ setResult(null);
2076
+ setError(null);
2077
+ },
2078
+ run: (capability, args = {}) => {
2079
+ if (ai === null || conversationId === null) return;
2080
+ abortRef.current?.abort();
2081
+ const controller = new AbortController();
2082
+ abortRef.current = controller;
2083
+ setRunning(true);
2084
+ setError(null);
2085
+ const base = {
2086
+ conversationId,
2087
+ messages: conversation.messages,
2088
+ participants: conversation.participants,
2089
+ signal: controller.signal
2090
+ };
2091
+ const call = () => {
2092
+ switch (capability) {
2093
+ case "catchUp": {
2094
+ const summary = conversation.summary;
2095
+ const self = summary?.participants.find(
2096
+ (participant) => participant.userId === host?.identity.userId
2097
+ );
2098
+ void self;
2099
+ return ai.catchMeUp({ ...base, since: null });
2100
+ }
2101
+ case "summarize":
2102
+ return ai.summarize(base);
2103
+ case "actionItems":
2104
+ return ai.extractActionItems(base);
2105
+ case "draftReply":
2106
+ return ai.draftReply({
2107
+ ...base,
2108
+ ...args.instruction !== void 0 ? { instruction: args.instruction } : {}
2109
+ });
2110
+ default:
2111
+ return Promise.reject(new Error(`Unknown capability ${String(capability)}`));
2112
+ }
2113
+ };
2114
+ call().then((next) => {
2115
+ if (!controller.signal.aborted) setResult(next);
2116
+ }).catch((cause) => {
2117
+ if (controller.signal.aborted) return;
2118
+ setError(String(cause?.message ?? cause));
2119
+ }).finally(() => {
2120
+ if (!controller.signal.aborted) setRunning(false);
2121
+ });
2122
+ }
2123
+ };
2124
+ }
2125
+ function useMessageAction(message) {
2126
+ const host = useRequiredMessagingHost();
2127
+ const action = message.action;
2128
+ const [receipt, setReceipt] = useState(
2129
+ () => action === null ? null : host.actions.receiptFor(action.kind, message.id, host.identity.userId)
2130
+ );
2131
+ const [isRunning, setRunning] = useState(false);
2132
+ return {
2133
+ choices: action === null ? [] : host.actions.choicesFor(action),
2134
+ receipt,
2135
+ isRunning,
2136
+ execute: (choice) => {
2137
+ if (action === null || isRunning || receipt !== null) return;
2138
+ setRunning(true);
2139
+ const context = {
2140
+ messageId: message.id,
2141
+ actorId: host.identity.userId,
2142
+ organizationId: host.identity.organizationId,
2143
+ choice
2144
+ };
2145
+ host.actions.execute(action, context).then(setReceipt).catch((cause) => {
2146
+ setReceipt({
2147
+ kind: action.kind,
2148
+ messageId: message.id,
2149
+ actorId: host.identity.userId,
2150
+ outcome: "declined",
2151
+ label: "Could not complete",
2152
+ settledAt: (/* @__PURE__ */ new Date()).toISOString(),
2153
+ detail: String(cause?.message ?? cause)
2154
+ });
2155
+ }).finally(() => setRunning(false));
2156
+ }
2157
+ };
2158
+ }
2159
+
2160
+ // src/react/components.tsx
2161
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
2162
+
2163
+ // src/core/actor.ts
2164
+ function readActorHint(metadata) {
2165
+ const raw = metadata["actor"];
2166
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
2167
+ const record = raw;
2168
+ const agentId = record["agentId"] ?? record["agent_id"];
2169
+ if (typeof agentId !== "string" || agentId.length === 0) return null;
2170
+ const name = record["agentName"] ?? record["agent_name"];
2171
+ const avatar = record["agentAvatarUrl"] ?? record["agent_avatar_url"];
2172
+ return {
2173
+ agentId,
2174
+ ...typeof name === "string" && name.length > 0 ? { agentName: name } : {},
2175
+ ...typeof avatar === "string" && avatar.length > 0 ? { agentAvatarUrl: avatar } : {}
2176
+ };
2177
+ }
2178
+ function resolveActor(message, sender) {
2179
+ const hint = readActorHint(message.metadata);
2180
+ const humanName = sender?.displayName ?? message.senderId;
2181
+ if (hint !== null) {
2182
+ return {
2183
+ // An agent NEVER inherits the human's name or avatar. When the agent did
2184
+ // not name itself, it is labeled generically — an honest "Agent" beats a
2185
+ // colleague's face on a message they did not write.
2186
+ displayName: hint.agentName ?? "Agent",
2187
+ avatarUrl: hint.agentAvatarUrl ?? null,
2188
+ isAgent: true,
2189
+ onBehalfOfName: humanName,
2190
+ principalUserId: message.senderId
2191
+ };
2192
+ }
2193
+ if (sender?.isAgent === true) {
2194
+ return {
2195
+ displayName: sender.displayName,
2196
+ avatarUrl: sender.avatarUrl,
2197
+ isAgent: true,
2198
+ onBehalfOfName: null,
2199
+ principalUserId: message.senderId
2200
+ };
2201
+ }
2202
+ return {
2203
+ displayName: humanName,
2204
+ avatarUrl: sender?.avatarUrl ?? null,
2205
+ isAgent: false,
2206
+ onBehalfOfName: null,
2207
+ principalUserId: message.senderId
2208
+ };
2209
+ }
2210
+
2211
+ // src/react/icons.tsx
2212
+ import { jsx as jsx2 } from "react/jsx-runtime";
2213
+ function Icon(props) {
2214
+ const { paths: declared, ...rest } = props;
2215
+ const paths = typeof declared === "string" ? [declared] : declared;
2216
+ return /* @__PURE__ */ jsx2(
2217
+ "svg",
2218
+ {
2219
+ viewBox: "0 0 24 24",
2220
+ width: "1em",
2221
+ height: "1em",
2222
+ fill: "none",
2223
+ stroke: "currentColor",
2224
+ strokeWidth: 2,
2225
+ strokeLinecap: "round",
2226
+ strokeLinejoin: "round",
2227
+ "aria-hidden": "true",
2228
+ focusable: "false",
2229
+ ...rest,
2230
+ children: paths.map((path) => /* @__PURE__ */ jsx2("path", { d: path }, path))
2231
+ }
2232
+ );
2233
+ }
2234
+ var SendIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M22 2 11 13", "M22 2l-7 20-4-9-9-4 20-7z"] });
2235
+ var PaperclipIcon = (props) => /* @__PURE__ */ jsx2(
2236
+ Icon,
2237
+ {
2238
+ ...props,
2239
+ paths: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"
2240
+ }
2241
+ );
2242
+ var SparklesIcon = (props) => /* @__PURE__ */ jsx2(
2243
+ Icon,
2244
+ {
2245
+ ...props,
2246
+ paths: [
2247
+ "M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3z",
2248
+ "M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9L19 15z"
2249
+ ]
2250
+ }
2251
+ );
2252
+ var CheckIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: "M20 6L9 17l-5-5" });
2253
+ var DoubleCheckIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M18 6L7 17l-4-4", "M22 6l-8.5 8.5"] });
2254
+ var ClockIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 7v5l3 2"] });
2255
+ var AlertIcon = (props) => /* @__PURE__ */ jsx2(
2256
+ Icon,
2257
+ {
2258
+ ...props,
2259
+ paths: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 8v5", "M12 16.5v.01"]
2260
+ }
2261
+ );
2262
+ var SearchIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", "M21 21l-4.3-4.3"] });
2263
+ var PlusIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M12 5v14", "M5 12h14"] });
2264
+ var CloseIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M18 6L6 18", "M6 6l12 12"] });
2265
+ var ChevronLeftIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: "M15 18l-6-6 6-6" });
2266
+ var ReplyIcon = (props) => /* @__PURE__ */ jsx2(Icon, { ...props, paths: ["M9 17l-5-5 5-5", "M4 12h11a5 5 0 0 1 5 5v3"] });
2267
+ var LinkIcon = (props) => /* @__PURE__ */ jsx2(
2268
+ Icon,
2269
+ {
2270
+ ...props,
2271
+ paths: [
2272
+ "M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
2273
+ "M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
2274
+ ]
2275
+ }
2276
+ );
2277
+ var BotIcon = (props) => /* @__PURE__ */ jsx2(
2278
+ Icon,
2279
+ {
2280
+ ...props,
2281
+ paths: [
2282
+ "M12 3v4",
2283
+ "M6 7h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2z",
2284
+ "M9 13v.01",
2285
+ "M15 13v.01"
2286
+ ]
2287
+ }
2288
+ );
2289
+ var UsersIcon = (props) => /* @__PURE__ */ jsx2(
2290
+ Icon,
2291
+ {
2292
+ ...props,
2293
+ paths: [
2294
+ "M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",
2295
+ "M9 10a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z",
2296
+ "M22 20v-2a4 4 0 0 0-3-3.87",
2297
+ "M16 3.13a4 4 0 0 1 0 7.75"
2298
+ ]
2299
+ }
2300
+ );
2301
+
2302
+ // src/react/parts.tsx
2303
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
2304
+ function Avatar(props) {
2305
+ const seed = props.seed ?? props.name;
2306
+ const paletteIndex = avatarPaletteIndex(seed);
2307
+ const className = `mx-msg__avatar${props.small === true ? " mx-msg__avatar--sm" : ""}`;
2308
+ return /* @__PURE__ */ jsxs(
2309
+ "span",
2310
+ {
2311
+ className,
2312
+ style: { background: `var(--mx-msg-avatar-${paletteIndex})` },
2313
+ "aria-hidden": "true",
2314
+ children: [
2315
+ props.imageUrl != null && props.imageUrl.length > 0 ? /* @__PURE__ */ jsx3("img", { className: "mx-msg__avatar-img", src: props.imageUrl, alt: "", loading: "lazy" }) : getInitials(props.name),
2316
+ props.online === true ? /* @__PURE__ */ jsx3("i", { className: "mx-msg__presence" }) : null
2317
+ ]
2318
+ }
2319
+ );
2320
+ }
2321
+ function DeliveryTick(props) {
2322
+ switch (props.state) {
2323
+ case "sending":
2324
+ return /* @__PURE__ */ jsx3(ClockIcon, { "aria-label": "Sending", role: "img" });
2325
+ case "sent":
2326
+ return /* @__PURE__ */ jsx3(CheckIcon, { "aria-label": "Sent", role: "img" });
2327
+ case "delivered":
2328
+ return /* @__PURE__ */ jsx3(DoubleCheckIcon, { "aria-label": "Delivered", role: "img" });
2329
+ case "read":
2330
+ return /* @__PURE__ */ jsx3(
2331
+ DoubleCheckIcon,
2332
+ {
2333
+ "aria-label": "Read",
2334
+ role: "img",
2335
+ style: { color: "var(--mx-msg-accent)" }
2336
+ }
2337
+ );
2338
+ case "failed":
2339
+ return /* @__PURE__ */ jsx3(AlertIcon, { "aria-label": "Not sent", role: "img" });
2340
+ default:
2341
+ return null;
2342
+ }
2343
+ }
2344
+ function ReferenceCard(props) {
2345
+ const { reference, onOpen } = props;
2346
+ const href = reference.href;
2347
+ if (onOpen == null && (href === void 0 || href.length === 0)) {
2348
+ return /* @__PURE__ */ jsxs(
2349
+ "span",
2350
+ {
2351
+ className: "mx-msg__reference mx-msg__reference--inert",
2352
+ title: "This app has not wired reference opening. Pass onOpenReference to <MessagingProvider> to make it open.",
2353
+ children: [
2354
+ /* @__PURE__ */ jsx3(LinkIcon, {}),
2355
+ /* @__PURE__ */ jsx3("span", { className: "mx-msg__reference-type", children: reference.entityType }),
2356
+ reference.label
2357
+ ]
2358
+ }
2359
+ );
2360
+ }
2361
+ return /* @__PURE__ */ jsxs(
2362
+ "button",
2363
+ {
2364
+ type: "button",
2365
+ className: "mx-msg__reference",
2366
+ onClick: () => {
2367
+ if (onOpen != null) onOpen(reference);
2368
+ else if (href !== void 0) globalThis.location?.assign(href);
2369
+ },
2370
+ children: [
2371
+ /* @__PURE__ */ jsx3(LinkIcon, {}),
2372
+ /* @__PURE__ */ jsx3("span", { className: "mx-msg__reference-type", children: reference.entityType }),
2373
+ reference.label
2374
+ ]
2375
+ }
2376
+ );
2377
+ }
2378
+ function AgentTag() {
2379
+ return /* @__PURE__ */ jsxs("span", { className: "mx-msg__agent-tag", children: [
2380
+ /* @__PURE__ */ jsx3(BotIcon, {}),
2381
+ "Agent"
2382
+ ] });
2383
+ }
2384
+ function TypingDots() {
2385
+ return /* @__PURE__ */ jsxs("span", { className: "mx-msg__dots", "aria-hidden": "true", children: [
2386
+ /* @__PURE__ */ jsx3("i", {}),
2387
+ /* @__PURE__ */ jsx3("i", {}),
2388
+ /* @__PURE__ */ jsx3("i", {})
2389
+ ] });
2390
+ }
2391
+ function ConversationSkeleton(props) {
2392
+ return /* @__PURE__ */ jsx3("div", { "aria-busy": "true", "aria-label": "Loading conversations", children: Array.from({ length: props.rows ?? 6 }, (_, index) => /* @__PURE__ */ jsxs("div", { className: "mx-msg__skeleton-row", children: [
2393
+ /* @__PURE__ */ jsx3(
2394
+ "span",
2395
+ {
2396
+ className: "mx-msg__skeleton",
2397
+ style: { width: 38, height: 38, borderRadius: "50%" }
2398
+ }
2399
+ ),
2400
+ /* @__PURE__ */ jsxs("span", { style: { flex: 1, display: "grid", gap: 6 }, children: [
2401
+ /* @__PURE__ */ jsx3("span", { className: "mx-msg__skeleton", style: { height: 11, width: "45%" } }),
2402
+ /* @__PURE__ */ jsx3("span", { className: "mx-msg__skeleton", style: { height: 10, width: "78%" } })
2403
+ ] })
2404
+ ] }, index)) });
2405
+ }
2406
+ function EmptyState(props) {
2407
+ return /* @__PURE__ */ jsxs("div", { className: "mx-msg__empty", children: [
2408
+ /* @__PURE__ */ jsx3("h3", { children: props.title }),
2409
+ props.body !== void 0 ? /* @__PURE__ */ jsx3("p", { children: props.body }) : null,
2410
+ props.action
2411
+ ] });
2412
+ }
2413
+
2414
+ // src/react/components.tsx
2415
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
2416
+ var AI_LABELS = {
2417
+ catchUp: "Catch me up",
2418
+ summarize: "Summarize",
2419
+ actionItems: "Action items",
2420
+ draftReply: "Draft a reply"
2421
+ };
2422
+ function ConversationList(props) {
2423
+ const { conversations, hasMore, isInitialLoading, loadMore, select, activeConversationId } = useConversations();
2424
+ const [query, setQuery] = useState2("");
2425
+ const visible = useMemo3(() => {
2426
+ const needle = query.trim().toLowerCase();
2427
+ if (needle.length === 0) return conversations;
2428
+ return conversations.filter(
2429
+ (item) => item.displayName.toLowerCase().includes(needle) || (item.lastMessageContent ?? "").toLowerCase().includes(needle)
2430
+ );
2431
+ }, [conversations, query]);
2432
+ return /* @__PURE__ */ jsxs2("div", { className: `mx-msg__list${props.className !== void 0 ? ` ${props.className}` : ""}`, children: [
2433
+ /* @__PURE__ */ jsxs2("div", { className: "mx-msg__search", children: [
2434
+ /* @__PURE__ */ jsx4(SearchIcon, { style: { color: "var(--mx-msg-text-faint)" } }),
2435
+ /* @__PURE__ */ jsx4(
2436
+ "input",
2437
+ {
2438
+ type: "search",
2439
+ value: query,
2440
+ placeholder: "Search conversations",
2441
+ "aria-label": "Search conversations",
2442
+ onChange: (event) => setQuery(event.target.value)
2443
+ }
2444
+ ),
2445
+ props.onNewConversation !== void 0 ? /* @__PURE__ */ jsx4(
2446
+ "button",
2447
+ {
2448
+ type: "button",
2449
+ className: "mx-msg__button mx-msg__button--ghost",
2450
+ style: { minWidth: 36, minHeight: 36 },
2451
+ onClick: props.onNewConversation,
2452
+ "aria-label": "New conversation",
2453
+ children: /* @__PURE__ */ jsx4(PlusIcon, {})
2454
+ }
2455
+ ) : null
2456
+ ] }),
2457
+ /* @__PURE__ */ jsxs2("div", { className: "mx-msg__scroll", children: [
2458
+ isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : visible.length === 0 ? /* @__PURE__ */ jsx4(
2459
+ EmptyState,
2460
+ {
2461
+ title: query.length > 0 ? "No matches" : "No conversations yet",
2462
+ body: query.length > 0 ? "Try a different name or word." : "Start one and it will appear here."
2463
+ }
2464
+ ) : /* @__PURE__ */ jsx4("ul", { className: "mx-msg__rows", children: visible.map((item) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(
2465
+ ConversationRow,
2466
+ {
2467
+ summary: item,
2468
+ isActive: item.conversation.id === activeConversationId,
2469
+ onSelect: () => {
2470
+ select(item.conversation.id);
2471
+ props.onSelect?.(item.conversation.id);
2472
+ }
2473
+ }
2474
+ ) }, item.conversation.id)) }),
2475
+ hasMore && !isInitialLoading ? /* @__PURE__ */ jsx4(
2476
+ "button",
2477
+ {
2478
+ type: "button",
2479
+ className: "mx-msg__chip",
2480
+ style: { margin: "8px auto", display: "block" },
2481
+ onClick: loadMore,
2482
+ children: "Load older conversations"
2483
+ }
2484
+ ) : null
2485
+ ] })
2486
+ ] });
2487
+ }
2488
+ function ConversationRow(props) {
2489
+ const { summary } = props;
2490
+ return /* @__PURE__ */ jsxs2(
2491
+ "button",
2492
+ {
2493
+ type: "button",
2494
+ className: "mx-msg__row",
2495
+ "aria-current": props.isActive,
2496
+ onClick: props.onSelect,
2497
+ children: [
2498
+ /* @__PURE__ */ jsx4(
2499
+ Avatar,
2500
+ {
2501
+ name: summary.displayName,
2502
+ imageUrl: summary.displayImageUrl,
2503
+ seed: summary.conversation.id
2504
+ }
2505
+ ),
2506
+ /* @__PURE__ */ jsxs2("span", { className: "mx-msg__row-body", children: [
2507
+ /* @__PURE__ */ jsxs2("span", { className: "mx-msg__row-top", children: [
2508
+ /* @__PURE__ */ jsx4("span", { className: "mx-msg__row-name", children: summary.displayName }),
2509
+ /* @__PURE__ */ jsx4("span", { className: "mx-msg__row-time", children: formatConversationTime(summary.lastMessageAt) })
2510
+ ] }),
2511
+ /* @__PURE__ */ jsxs2("span", { className: "mx-msg__row-preview", children: [
2512
+ /* @__PURE__ */ jsx4("span", { children: summarizeText(summary.lastMessageContent ?? "", 90) || "No messages yet" }),
2513
+ summary.unreadCount > 0 ? /* @__PURE__ */ jsx4("span", { className: "mx-msg__badge", "aria-label": `${summary.unreadCount} unread`, children: summary.unreadCount > 99 ? "99+" : summary.unreadCount }) : null
2514
+ ] })
2515
+ ] })
2516
+ ]
2517
+ }
2518
+ );
2519
+ }
2520
+ function ConversationView(props) {
2521
+ const { conversationId } = props;
2522
+ const host = useMessagingHost();
2523
+ const conversation = useConversation(conversationId);
2524
+ const typists = useTypists(conversationId, conversation.participants);
2525
+ const online = useOnlineUserIds(conversationId);
2526
+ const composer = useComposer(conversationId);
2527
+ const ai = useMessagingAi(conversationId);
2528
+ const scrollRef = useRef3(null);
2529
+ const selfId = host?.identity.userId ?? "";
2530
+ const messages = conversation.messages;
2531
+ const lastId = messages.at(-1)?.id ?? null;
2532
+ useEffect3(() => {
2533
+ const node = scrollRef.current;
2534
+ if (node === null) return;
2535
+ const distance = node.scrollHeight - node.scrollTop - node.clientHeight;
2536
+ if (distance < 240) node.scrollTop = node.scrollHeight;
2537
+ }, [lastId]);
2538
+ if (conversationId === null) {
2539
+ return /* @__PURE__ */ jsx4("div", { className: `mx-msg__thread${props.className !== void 0 ? ` ${props.className}` : ""}`, children: /* @__PURE__ */ jsx4(
2540
+ EmptyState,
2541
+ {
2542
+ title: "Pick a conversation",
2543
+ body: "Your messages, mentions, and agent replies live here."
2544
+ }
2545
+ ) });
2546
+ }
2547
+ const others = conversation.participants.filter(
2548
+ (participant) => participant.userId !== selfId
2549
+ );
2550
+ const anyOnline = others.some((participant) => online.has(participant.userId));
2551
+ return /* @__PURE__ */ jsxs2("div", { className: `mx-msg__thread${props.className !== void 0 ? ` ${props.className}` : ""}`, children: [
2552
+ /* @__PURE__ */ jsxs2("header", { className: "mx-msg__header", children: [
2553
+ props.onBack !== void 0 ? /* @__PURE__ */ jsx4(
2554
+ "button",
2555
+ {
2556
+ type: "button",
2557
+ className: "mx-msg__button mx-msg__button--ghost",
2558
+ style: { minWidth: 36, minHeight: 36 },
2559
+ onClick: props.onBack,
2560
+ "aria-label": "Back to conversations",
2561
+ children: /* @__PURE__ */ jsx4(ChevronLeftIcon, {})
2562
+ }
2563
+ ) : null,
2564
+ /* @__PURE__ */ jsx4(
2565
+ Avatar,
2566
+ {
2567
+ name: conversation.summary?.displayName ?? "Conversation",
2568
+ imageUrl: conversation.summary?.displayImageUrl ?? null,
2569
+ seed: conversationId,
2570
+ online: anyOnline,
2571
+ small: true
2572
+ }
2573
+ ),
2574
+ /* @__PURE__ */ jsxs2("span", { style: { minWidth: 0 }, children: [
2575
+ /* @__PURE__ */ jsx4("span", { className: "mx-msg__header-title", children: conversation.summary?.displayName ?? "Conversation" }),
2576
+ /* @__PURE__ */ jsx4("span", { className: "mx-msg__header-subtitle", children: typists.label ?? (conversation.summary?.conversation.type === "direct" ? anyOnline ? "Active now" : "" : `${conversation.participants.length} members`) })
2577
+ ] }),
2578
+ /* @__PURE__ */ jsx4("span", { className: "mx-msg__header-actions", children: conversation.summary?.conversation.type !== "direct" ? /* @__PURE__ */ jsx4(
2579
+ "span",
2580
+ {
2581
+ className: "mx-msg__button mx-msg__button--ghost",
2582
+ style: { minWidth: 36, minHeight: 36 },
2583
+ "aria-label": `${conversation.participants.length} members`,
2584
+ children: /* @__PURE__ */ jsx4(UsersIcon, {})
2585
+ }
2586
+ ) : null })
2587
+ ] }),
2588
+ ai.available.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mx-msg__ai-bar", children: ai.available.map((capability) => /* @__PURE__ */ jsxs2(
2589
+ "button",
2590
+ {
2591
+ type: "button",
2592
+ className: "mx-msg__chip",
2593
+ disabled: ai.isRunning,
2594
+ onClick: () => ai.run(capability),
2595
+ children: [
2596
+ /* @__PURE__ */ jsx4(SparklesIcon, { style: { marginRight: 4, verticalAlign: "-0.12em" } }),
2597
+ AI_LABELS[capability]
2598
+ ]
2599
+ },
2600
+ capability
2601
+ )) }) : null,
2602
+ ai.isRunning ? /* @__PURE__ */ jsx4("p", { className: "mx-msg__ai-output", "aria-busy": "true", children: /* @__PURE__ */ jsx4("span", { className: "mx-msg__skeleton", style: { display: "block", height: 11, width: "72%" } }) }) : ai.error !== null ? /* @__PURE__ */ jsx4("p", { className: "mx-msg__ai-output", style: { color: "var(--mx-msg-danger)" }, children: ai.error }) : ai.result !== null ? /* @__PURE__ */ jsxs2("p", { className: "mx-msg__ai-output", children: [
2603
+ ai.result.text,
2604
+ /* @__PURE__ */ jsx4(
2605
+ "button",
2606
+ {
2607
+ type: "button",
2608
+ className: "mx-msg__button mx-msg__button--ghost",
2609
+ style: { minWidth: 28, minHeight: 28, float: "right" },
2610
+ onClick: ai.clear,
2611
+ "aria-label": "Dismiss",
2612
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
2613
+ }
2614
+ )
2615
+ ] }) : null,
2616
+ /* @__PURE__ */ jsx4("div", { className: "mx-msg__scroll", ref: scrollRef, children: conversation.isLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, { rows: 4 }) : messages.length === 0 ? /* @__PURE__ */ jsx4(EmptyState, { title: "No messages yet", body: "Say something to get started." }) : /* @__PURE__ */ jsxs2("div", { className: "mx-msg__messages", children: [
2617
+ conversation.hasMoreOlder ? /* @__PURE__ */ jsx4(
2618
+ "button",
2619
+ {
2620
+ type: "button",
2621
+ className: "mx-msg__chip",
2622
+ style: { margin: "4px auto 12px", display: "block" },
2623
+ onClick: conversation.loadOlder,
2624
+ children: "Load earlier messages"
2625
+ }
2626
+ ) : null,
2627
+ groupMessages(messages).map((group) => {
2628
+ const first = group.messages[0];
2629
+ if (first === void 0) return null;
2630
+ const isMine = group.senderId === selfId;
2631
+ const sender = conversation.participants.find(
2632
+ (participant) => participant.userId === group.senderId
2633
+ ) ?? null;
2634
+ return /* @__PURE__ */ jsxs2("div", { children: [
2635
+ group.dateSeparator !== null ? /* @__PURE__ */ jsx4("div", { className: "mx-msg__day", children: group.dateSeparator }) : null,
2636
+ /* @__PURE__ */ jsx4(
2637
+ MessageGroupView,
2638
+ {
2639
+ messages: group.messages,
2640
+ sender,
2641
+ isMine,
2642
+ onRetry: conversation.retry,
2643
+ onReply: composer.setReplyTo,
2644
+ pendingIds: new Set(
2645
+ conversation.pending.filter((entry) => entry.state === "failed").map((entry) => entry.clientMessageId)
2646
+ ),
2647
+ failedEntryIdByClientId: new Map(
2648
+ conversation.pending.map((entry) => [entry.clientMessageId, entry.id])
2649
+ )
2650
+ }
2651
+ )
2652
+ ] }, first.id);
2653
+ })
2654
+ ] }) }),
2655
+ /* @__PURE__ */ jsx4("div", { className: "mx-msg__typing", "aria-live": "polite", children: typists.label !== null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2656
+ /* @__PURE__ */ jsx4(TypingDots, {}),
2657
+ typists.label
2658
+ ] }) : null }),
2659
+ /* @__PURE__ */ jsx4(Composer, { conversationId })
2660
+ ] });
2661
+ }
2662
+ function MessageGroupView(props) {
2663
+ const first = props.messages[0];
2664
+ if (first === void 0) return null;
2665
+ const actor = resolveActor(first, props.sender);
2666
+ return /* @__PURE__ */ jsxs2("div", { className: `mx-msg__group${props.isMine ? " mx-msg__group--mine" : ""}`, children: [
2667
+ /* @__PURE__ */ jsx4(
2668
+ Avatar,
2669
+ {
2670
+ name: actor.displayName,
2671
+ imageUrl: actor.avatarUrl,
2672
+ seed: actor.isAgent ? `agent:${actor.displayName}` : actor.principalUserId,
2673
+ small: true
2674
+ }
2675
+ ),
2676
+ /* @__PURE__ */ jsxs2("div", { className: "mx-msg__group-body", children: [
2677
+ !props.isMine || actor.isAgent ? /* @__PURE__ */ jsxs2("div", { className: "mx-msg__author", children: [
2678
+ actor.displayName,
2679
+ actor.isAgent ? /* @__PURE__ */ jsx4(AgentTag, {}) : null,
2680
+ actor.onBehalfOfName !== null ? /* @__PURE__ */ jsxs2("span", { style: { fontWeight: 400, color: "var(--mx-msg-text-faint)" }, children: [
2681
+ "via ",
2682
+ actor.onBehalfOfName
2683
+ ] }) : null
2684
+ ] }) : null,
2685
+ props.messages.map((message, index) => /* @__PURE__ */ jsx4(
2686
+ MessageBubble,
2687
+ {
2688
+ message,
2689
+ isMine: props.isMine,
2690
+ isLast: index === props.messages.length - 1,
2691
+ onRetry: props.onRetry,
2692
+ onReply: props.onReply,
2693
+ failedEntryId: message.clientMessageId !== null ? props.failedEntryIdByClientId.get(message.clientMessageId) ?? null : null
2694
+ },
2695
+ message.id
2696
+ ))
2697
+ ] })
2698
+ ] });
2699
+ }
2700
+ function MessageBubble(props) {
2701
+ const { message, isMine } = props;
2702
+ const host = useMessagingHost();
2703
+ if (message.deletedAt !== null) {
2704
+ return /* @__PURE__ */ jsx4("div", { className: "mx-msg__bubble mx-msg__bubble--deleted", children: "Message deleted" });
2705
+ }
2706
+ const classes = [
2707
+ "mx-msg__bubble",
2708
+ isMine ? "mx-msg__bubble--mine" : "",
2709
+ props.isLast ? "mx-msg__bubble--tail" : "",
2710
+ message.deliveryState === "sending" ? "mx-msg__bubble--pending" : "",
2711
+ message.deliveryState === "failed" ? "mx-msg__bubble--failed" : ""
2712
+ ].filter(Boolean).join(" ");
2713
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2714
+ /* @__PURE__ */ jsxs2("div", { className: classes, children: [
2715
+ splitText(message.content).map(
2716
+ (segment, index) => segment.type === "text" ? /* @__PURE__ */ jsx4("span", { children: segment.value }, index) : /* @__PURE__ */ jsx4(
2717
+ ReferenceCard,
2718
+ {
2719
+ reference: segment.reference,
2720
+ onOpen: host?.openReference ?? null
2721
+ },
2722
+ `${segment.reference.entityType}:${segment.reference.entityId}`
2723
+ )
2724
+ ),
2725
+ message.references.map((reference) => /* @__PURE__ */ jsx4(
2726
+ ReferenceCard,
2727
+ {
2728
+ reference,
2729
+ onOpen: host?.openReference ?? null
2730
+ },
2731
+ `structured:${reference.entityType}:${reference.entityId}`
2732
+ )),
2733
+ message.action !== null ? /* @__PURE__ */ jsx4(MessageActionChips, { message }) : null
2734
+ ] }),
2735
+ /* @__PURE__ */ jsxs2(
2736
+ "div",
2737
+ {
2738
+ className: `mx-msg__meta${message.deliveryState === "failed" ? " mx-msg__meta--failed" : ""}`,
2739
+ children: [
2740
+ props.isLast ? formatMessageTime(message.createdAt) : null,
2741
+ message.editedAt !== null ? " \xB7 edited" : null,
2742
+ isMine ? /* @__PURE__ */ jsx4(DeliveryTick, { state: message.deliveryState }) : null,
2743
+ message.deliveryState === "failed" && props.failedEntryId != null && props.onRetry !== void 0 ? /* @__PURE__ */ jsx4(
2744
+ "button",
2745
+ {
2746
+ type: "button",
2747
+ className: "mx-msg__chip mx-msg__chip--danger",
2748
+ style: { minHeight: 24, padding: "2px 8px" },
2749
+ onClick: () => props.onRetry?.(props.failedEntryId),
2750
+ children: "Retry"
2751
+ }
2752
+ ) : null,
2753
+ props.onReply !== void 0 && message.deliveryState !== "sending" ? /* @__PURE__ */ jsx4(
2754
+ "button",
2755
+ {
2756
+ type: "button",
2757
+ className: "mx-msg__button mx-msg__button--ghost",
2758
+ style: { minWidth: 24, minHeight: 24, fontSize: 13 },
2759
+ onClick: () => props.onReply?.(message),
2760
+ "aria-label": "Reply",
2761
+ children: /* @__PURE__ */ jsx4(ReplyIcon, {})
2762
+ }
2763
+ ) : null
2764
+ ]
2765
+ }
2766
+ )
2767
+ ] });
2768
+ }
2769
+ function MessageActionChips(props) {
2770
+ const action = useMessageAction(props.message);
2771
+ if (action.receipt !== null) {
2772
+ return /* @__PURE__ */ jsxs2("span", { className: "mx-msg__receipt", children: [
2773
+ action.receipt.label,
2774
+ action.receipt.outcome === "already" ? " (already done)" : ""
2775
+ ] });
2776
+ }
2777
+ if (action.choices.length === 0) return null;
2778
+ return /* @__PURE__ */ jsx4("div", { className: "mx-msg__chips", children: action.choices.map((choice) => /* @__PURE__ */ jsx4(
2779
+ "button",
2780
+ {
2781
+ type: "button",
2782
+ className: `mx-msg__chip${choice.tone === "primary" ? " mx-msg__chip--primary" : choice.tone === "danger" ? " mx-msg__chip--danger" : ""}`,
2783
+ disabled: action.isRunning,
2784
+ onClick: () => action.execute(choice.id),
2785
+ children: choice.label
2786
+ },
2787
+ choice.id
2788
+ )) });
2789
+ }
2790
+ function Composer(props) {
2791
+ const composer = useComposer(props.conversationId);
2792
+ const textareaRef = useRef3(null);
2793
+ useEffect3(() => {
2794
+ const node = textareaRef.current;
2795
+ if (node === null) return;
2796
+ node.style.height = "auto";
2797
+ node.style.height = `${node.scrollHeight}px`;
2798
+ }, [composer.value]);
2799
+ return /* @__PURE__ */ jsxs2("div", { className: "mx-msg__composer", children: [
2800
+ composer.replyTo !== null ? /* @__PURE__ */ jsxs2("div", { className: "mx-msg__reply-bar", children: [
2801
+ /* @__PURE__ */ jsx4(ReplyIcon, {}),
2802
+ /* @__PURE__ */ jsx4("span", { children: summarizeText(composer.replyTo.content, 70) }),
2803
+ /* @__PURE__ */ jsx4(
2804
+ "button",
2805
+ {
2806
+ type: "button",
2807
+ className: "mx-msg__button mx-msg__button--ghost",
2808
+ style: { minWidth: 26, minHeight: 26, marginLeft: "auto" },
2809
+ onClick: () => composer.setReplyTo(null),
2810
+ "aria-label": "Cancel reply",
2811
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
2812
+ }
2813
+ )
2814
+ ] }) : null,
2815
+ /* @__PURE__ */ jsxs2("div", { className: "mx-msg__composer-row", children: [
2816
+ /* @__PURE__ */ jsx4(
2817
+ "textarea",
2818
+ {
2819
+ ref: textareaRef,
2820
+ className: "mx-msg__input",
2821
+ rows: 1,
2822
+ value: composer.value,
2823
+ placeholder: "Message",
2824
+ "aria-label": "Message",
2825
+ disabled: props.conversationId === null,
2826
+ onChange: (event) => {
2827
+ composer.setValue(event.target.value);
2828
+ composer.onKeystroke();
2829
+ },
2830
+ onKeyDown: (event) => {
2831
+ if (event.key !== "Enter" || event.shiftKey) return;
2832
+ const isTouch = typeof globalThis.matchMedia === "function" && globalThis.matchMedia("(pointer: coarse)").matches;
2833
+ if (isTouch) return;
2834
+ event.preventDefault();
2835
+ composer.send();
2836
+ }
2837
+ }
2838
+ ),
2839
+ /* @__PURE__ */ jsx4(
2840
+ "button",
2841
+ {
2842
+ type: "button",
2843
+ className: "mx-msg__button",
2844
+ disabled: !composer.canSend,
2845
+ onClick: composer.send,
2846
+ "aria-label": "Send",
2847
+ children: /* @__PURE__ */ jsx4(SendIcon, {})
2848
+ }
2849
+ )
2850
+ ] })
2851
+ ] });
2852
+ }
2853
+ function MessagingInbox(props) {
2854
+ const host = useMessagingHost();
2855
+ const [selected, setSelected] = useState2(props.conversationId ?? null);
2856
+ useEffect3(() => {
2857
+ if (props.conversationId !== void 0) setSelected(props.conversationId);
2858
+ }, [props.conversationId]);
2859
+ if (host === null) {
2860
+ return /* @__PURE__ */ jsxs2("div", { className: `mx-msg${props.className !== void 0 ? ` ${props.className}` : ""}`, children: [
2861
+ /* @__PURE__ */ jsx4("div", { className: "mx-msg__list", children: /* @__PURE__ */ jsx4(ConversationSkeleton, {}) }),
2862
+ /* @__PURE__ */ jsx4("div", { className: "mx-msg__thread" })
2863
+ ] });
2864
+ }
2865
+ const choose = (id) => {
2866
+ setSelected(id);
2867
+ props.onConversationChange?.(id);
2868
+ };
2869
+ return /* @__PURE__ */ jsxs2(
2870
+ "div",
2871
+ {
2872
+ className: `mx-msg${props.className !== void 0 ? ` ${props.className}` : ""}`,
2873
+ "data-pane": selected === null ? "list" : "thread",
2874
+ children: [
2875
+ /* @__PURE__ */ jsx4(
2876
+ ConversationList,
2877
+ {
2878
+ onSelect: choose,
2879
+ ...props.onNewConversation !== void 0 ? { onNewConversation: props.onNewConversation } : {}
2880
+ }
2881
+ ),
2882
+ /* @__PURE__ */ jsx4(ConversationView, { conversationId: selected, onBack: () => choose(null) })
2883
+ ]
2884
+ }
2885
+ );
2886
+ }
2887
+
2888
+ // src/core/types.ts
2889
+ var asConversationId = (value) => value;
2890
+ var asMessageId = (value) => value;
2891
+ var asUserId = (value) => value;
2892
+ var asOrganizationId = (value) => value;
2893
+ var asClientMessageId = (value) => value;
2894
+ export {
2895
+ AgentTag,
2896
+ AlertIcon,
2897
+ Avatar,
2898
+ BotIcon,
2899
+ CheckIcon,
2900
+ ChevronLeftIcon,
2901
+ ClockIcon,
2902
+ CloseIcon,
2903
+ Composer,
2904
+ ConversationList,
2905
+ ConversationSkeleton,
2906
+ ConversationView,
2907
+ DeliveryTick,
2908
+ DoubleCheckIcon,
2909
+ EmptyState,
2910
+ LinkIcon,
2911
+ MESSAGING_EVENTS,
2912
+ MESSAGING_SCHEMA,
2913
+ MessageActionChips,
2914
+ MessageBubble,
2915
+ MessagingError,
2916
+ MessagingInbox,
2917
+ MessagingProvider,
2918
+ PaperclipIcon,
2919
+ PlusIcon,
2920
+ RPCS,
2921
+ ReferenceCard,
2922
+ ReplyIcon,
2923
+ SearchIcon,
2924
+ SendIcon,
2925
+ SparklesIcon,
2926
+ TABLES,
2927
+ TypingDots,
2928
+ UsersIcon,
2929
+ asClientMessageId,
2930
+ asConversationId,
2931
+ asMessageId,
2932
+ asOrganizationId,
2933
+ asUserId,
2934
+ avatarPaletteIndex,
2935
+ composeFence,
2936
+ conversationTopic,
2937
+ createActionRegistry,
2938
+ createMemoryOutboxStorage,
2939
+ createMessagingAi,
2940
+ createMessagingEngine,
2941
+ createMessagingRepository,
2942
+ createMessagingStore,
2943
+ createOutbox,
2944
+ createReadCache,
2945
+ createWebOutboxStorage,
2946
+ extractReferences,
2947
+ formatConversationTime,
2948
+ formatDateSeparator,
2949
+ formatLastSeen,
2950
+ formatMessageTime,
2951
+ formatTypists,
2952
+ getInitials,
2953
+ groupMessages,
2954
+ inboxTopic,
2955
+ invalidResponse,
2956
+ isSameDay,
2957
+ messagingClientId,
2958
+ normalizeMessagingError,
2959
+ optimisticMessage,
2960
+ participantNames,
2961
+ projectConversationSummary,
2962
+ projectMessage,
2963
+ projectMessageAction,
2964
+ projectParticipantRole,
2965
+ projectUserSummary,
2966
+ resolveActor,
2967
+ splitText,
2968
+ summarizeText,
2969
+ useComposer,
2970
+ useConversation,
2971
+ useConversations,
2972
+ useMessageAction,
2973
+ useMessagingAi,
2974
+ useMessagingHost,
2975
+ useMessagingSnapshot,
2976
+ useOnlineUserIds,
2977
+ useRequiredMessagingHost,
2978
+ useTypists
2979
+ };
2980
+ //# sourceMappingURL=react.js.map