@ai-matrx/messaging 0.14.0 → 0.15.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.d.cts CHANGED
@@ -1019,11 +1019,18 @@ interface MessagingSnapshot {
1019
1019
  } | null;
1020
1020
  /** Conversations whose last open failed, keyed by id. Cleared on the next open. */
1021
1021
  readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
1022
+ /**
1023
+ * The inbox read FAILED (and no later read succeeded). Without it a failed
1024
+ * first read left `hasLoadedConversations` false forever — a skeleton that
1025
+ * never resolves is a screen telling a lie (RC-B12 round 11).
1026
+ */
1027
+ readonly conversationsLoadError: string | null;
1022
1028
  }
1023
1029
  interface MessagingStore {
1024
1030
  snapshot(): MessagingSnapshot;
1025
1031
  subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
1026
1032
  setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
1033
+ setConversationsLoadError(message: string | null): void;
1027
1034
  /**
1028
1035
  * Switch the archive axis. Clears the list and marks it UNLOADED: the rows
1029
1036
  * on screen belong to the old filter, and leaving them there while the new
@@ -1133,6 +1140,8 @@ interface MessagingEngine {
1133
1140
  * refreshes the archived count the reveal label prints.
1134
1141
  */
1135
1142
  setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
1143
+ /** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
1144
+ refreshInbox(): Promise<void>;
1136
1145
  /** Re-read how many archived conversations exist, for the reveal's label. */
1137
1146
  refreshArchivedCount(): Promise<void>;
1138
1147
  /** Open a conversation: load its thread and subscribe to its channel. */
@@ -1376,6 +1385,10 @@ interface UseConversationsResult {
1376
1385
  hasMore: boolean;
1377
1386
  /** True only on the FIRST load; a refresh must not blank the list. */
1378
1387
  isInitialLoading: boolean;
1388
+ /** The inbox read failed; the list shows the failure (never a skeleton forever, never "empty"). */
1389
+ loadError: string | null;
1390
+ /** Read the inbox again. */
1391
+ retry: () => void;
1379
1392
  totalUnreadConversations: number;
1380
1393
  loadMore: () => void;
1381
1394
  select: (id: ConversationId) => void;
package/dist/react.d.ts CHANGED
@@ -1019,11 +1019,18 @@ interface MessagingSnapshot {
1019
1019
  } | null;
1020
1020
  /** Conversations whose last open failed, keyed by id. Cleared on the next open. */
1021
1021
  readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
1022
+ /**
1023
+ * The inbox read FAILED (and no later read succeeded). Without it a failed
1024
+ * first read left `hasLoadedConversations` false forever — a skeleton that
1025
+ * never resolves is a screen telling a lie (RC-B12 round 11).
1026
+ */
1027
+ readonly conversationsLoadError: string | null;
1022
1028
  }
1023
1029
  interface MessagingStore {
1024
1030
  snapshot(): MessagingSnapshot;
1025
1031
  subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
1026
1032
  setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
1033
+ setConversationsLoadError(message: string | null): void;
1027
1034
  /**
1028
1035
  * Switch the archive axis. Clears the list and marks it UNLOADED: the rows
1029
1036
  * on screen belong to the old filter, and leaving them there while the new
@@ -1133,6 +1140,8 @@ interface MessagingEngine {
1133
1140
  * refreshes the archived count the reveal label prints.
1134
1141
  */
1135
1142
  setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
1143
+ /** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
1144
+ refreshInbox(): Promise<void>;
1136
1145
  /** Re-read how many archived conversations exist, for the reveal's label. */
1137
1146
  refreshArchivedCount(): Promise<void>;
1138
1147
  /** Open a conversation: load its thread and subscribe to its channel. */
@@ -1376,6 +1385,10 @@ interface UseConversationsResult {
1376
1385
  hasMore: boolean;
1377
1386
  /** True only on the FIRST load; a refresh must not blank the list. */
1378
1387
  isInitialLoading: boolean;
1388
+ /** The inbox read failed; the list shows the failure (never a skeleton forever, never "empty"). */
1389
+ loadError: string | null;
1390
+ /** Read the inbox again. */
1391
+ retry: () => void;
1379
1392
  totalUnreadConversations: number;
1380
1393
  loadMore: () => void;
1381
1394
  select: (id: ConversationId) => void;
package/dist/react.js CHANGED
@@ -876,6 +876,7 @@ function createMessagingStore() {
876
876
  let threads = /* @__PURE__ */ new Map();
877
877
  let activeConversationId = null;
878
878
  let conversationReadFailures = /* @__PURE__ */ new Map();
879
+ let conversationsLoadError = null;
879
880
  const listeners = /* @__PURE__ */ new Set();
880
881
  let cached = null;
881
882
  function snapshot() {
@@ -889,7 +890,8 @@ function createMessagingStore() {
889
890
  totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
890
891
  archiveFilter,
891
892
  archivedCount,
892
- conversationReadFailures
893
+ conversationReadFailures,
894
+ conversationsLoadError
893
895
  };
894
896
  return cached;
895
897
  }
@@ -934,6 +936,12 @@ function createMessagingStore() {
934
936
  conversations = normalizeConversations(items);
935
937
  hasMoreConversations = hasMore;
936
938
  hasLoadedConversations = true;
939
+ conversationsLoadError = null;
940
+ emit();
941
+ },
942
+ setConversationsLoadError(message) {
943
+ if (message === conversationsLoadError) return;
944
+ conversationsLoadError = message;
937
945
  emit();
938
946
  },
939
947
  setArchiveFilter(next) {
@@ -1166,10 +1174,20 @@ function createMessagingEngine(options) {
1166
1174
  }
1167
1175
  async function reloadInbox() {
1168
1176
  if (disposed) return;
1169
- const page = await repository.listConversations({
1170
- limit: conversationPageSize,
1171
- archived: store.snapshot().archiveFilter
1172
- });
1177
+ let page;
1178
+ try {
1179
+ page = await repository.listConversations({
1180
+ limit: conversationPageSize,
1181
+ archived: store.snapshot().archiveFilter
1182
+ });
1183
+ } catch (error) {
1184
+ if (!disposed) {
1185
+ store.setConversationsLoadError(
1186
+ error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
1187
+ );
1188
+ }
1189
+ throw error;
1190
+ }
1173
1191
  if (disposed) return;
1174
1192
  conversationCursor = page.nextCursor;
1175
1193
  store.setConversations(page.items, page.hasMore);
@@ -1254,6 +1272,13 @@ function createMessagingEngine(options) {
1254
1272
  }
1255
1273
  });
1256
1274
  },
1275
+ async refreshInbox() {
1276
+ try {
1277
+ await reloadInbox();
1278
+ } catch (error) {
1279
+ reportError(error, "inboxRefresh");
1280
+ }
1281
+ },
1257
1282
  async setArchiveFilter(next) {
1258
1283
  if (next === store.snapshot().archiveFilter) return;
1259
1284
  store.setArchiveFilter(next);
@@ -2397,7 +2422,11 @@ function useConversations() {
2397
2422
  hasMore: snapshot?.hasMoreConversations ?? false,
2398
2423
  // "Not loaded yet" is a FACT from the store, never a guess. Showing
2399
2424
  // "No conversations yet" before the first read is a screen telling a lie.
2400
- isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations,
2425
+ isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations && snapshot.conversationsLoadError === null,
2426
+ loadError: snapshot?.conversationsLoadError ?? null,
2427
+ retry: () => {
2428
+ void host?.engine.refreshInbox();
2429
+ },
2401
2430
  totalUnreadConversations: snapshot?.totalUnreadConversations ?? 0,
2402
2431
  loadMore: () => {
2403
2432
  void host?.engine.loadMoreConversations();
@@ -2984,6 +3013,7 @@ function EmptyState(props) {
2984
3013
  }
2985
3014
 
2986
3015
  // src/react/components.tsx
3016
+ import { ErrorBox } from "@ai-matrx/design-system";
2987
3017
  import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
2988
3018
  var AI_LABELS = {
2989
3019
  catchUp: "Catch me up",
@@ -3001,7 +3031,9 @@ function ConversationList(props) {
3001
3031
  activeConversationId,
3002
3032
  archiveFilter,
3003
3033
  setArchiveFilter,
3004
- archivedCount
3034
+ archivedCount,
3035
+ loadError,
3036
+ retry
3005
3037
  } = useConversations();
3006
3038
  const RowChrome = useMessagingHost()?.wrapConversationRow ?? null;
3007
3039
  const [query, setQuery] = useState2("");
@@ -3066,7 +3098,20 @@ function ConversationList(props) {
3066
3098
  }
3067
3099
  ) }) : null,
3068
3100
  /* @__PURE__ */ jsxs2("div", { className: "mx-msg__scroll", children: [
3069
- isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : visible.length === 0 ? /* @__PURE__ */ jsx4(
3101
+ isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : loadError !== null && visible.length === 0 ? (
3102
+ // A failed read resolves to the failure — never a skeleton forever,
3103
+ // never "No conversations yet" (RC-B12 round 11). ErrorBox is the
3104
+ // design-system error slot: the host fills it with its menu.
3105
+ /* @__PURE__ */ jsx4(
3106
+ ReadFailureBox,
3107
+ {
3108
+ title: "Couldn't load your conversations",
3109
+ message: loadError,
3110
+ operation: "List your conversations",
3111
+ onRetry: retry
3112
+ }
3113
+ )
3114
+ ) : visible.length === 0 ? /* @__PURE__ */ jsx4(
3070
3115
  EmptyState,
3071
3116
  {
3072
3117
  title: query.length > 0 ? "No matches" : showingArchive ? "No archived conversations" : "No conversations yet",
@@ -3243,10 +3288,11 @@ function ConversationView(props) {
3243
3288
  body: "It doesn't exist, or you aren't a member of it."
3244
3289
  }
3245
3290
  ) : conversation.error !== null && messages.length === 0 ? /* @__PURE__ */ jsx4(
3246
- EmptyState,
3291
+ ReadFailureBox,
3247
3292
  {
3248
3293
  title: "Couldn't load this conversation",
3249
- body: "Check your connection, then open it again."
3294
+ message: conversation.error.message || "Check your connection, then open it again.",
3295
+ operation: "Open this conversation"
3250
3296
  }
3251
3297
  ) : messages.length === 0 ? /* @__PURE__ */ jsx4(EmptyState, { title: "No messages yet", body: "Say something to get started." }) : /* @__PURE__ */ jsxs2("div", { className: "mx-msg__messages", children: [
3252
3298
  conversation.hasMoreOlder ? /* @__PURE__ */ jsx4(
@@ -3577,6 +3623,29 @@ function MessagingInbox(props) {
3577
3623
  }
3578
3624
  );
3579
3625
  }
3626
+ function ReadFailureBox({
3627
+ title,
3628
+ message,
3629
+ operation,
3630
+ onRetry
3631
+ }) {
3632
+ return /* @__PURE__ */ jsxs2("div", { className: "mx-msg__empty", role: "alert", children: [
3633
+ /* @__PURE__ */ jsx4(
3634
+ ErrorBox,
3635
+ {
3636
+ as: "div",
3637
+ title,
3638
+ message,
3639
+ operation,
3640
+ origin: "messaging",
3641
+ style: { color: "var(--mx-msg-danger)", fontWeight: 600 },
3642
+ children: title
3643
+ }
3644
+ ),
3645
+ /* @__PURE__ */ jsx4("p", { children: message }),
3646
+ onRetry ? /* @__PURE__ */ jsx4("button", { type: "button", className: "mx-msg__button mx-msg__button--ghost", onClick: onRetry, children: "Try again" }) : null
3647
+ ] });
3648
+ }
3580
3649
  export {
3581
3650
  AgentTag,
3582
3651
  AlertIcon,