@ttt-productions/chat-core 0.11.0 → 0.11.2

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/index.d.ts CHANGED
@@ -1,7 +1,4 @@
1
- export { MAX_CHAT_MESSAGE_LENGTH } from "./constants.js";
2
1
  export { GROUP_GAP_SEC } from "./types.js";
3
2
  export { isContinuation } from "./grouping.js";
4
- export type { ChatId, ChatThreadV1, ChatMessageV1, ChatAccessMode, ModerationHandlers, ChatNameResolver, ChatPrewarmSenders, } from "./types.js";
5
- export type { MentionRef, ParsedSegment, MentionProvider, RecentMentionsAdapter, MentionAnchor, } from "./mentions/types.js";
6
- export { parseMentionTokens, formatMentionToken } from "./mentions/parser.js";
3
+ export type { ChatId, ChatThreadV1, ChatMessageV1, ModerationHandlers, ChatNameResolver, ChatNameResolution, ChatPrewarmSenders, } from "./types.js";
7
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,MAAM,EACN,YAAY,EACZ,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,GACrB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACR,UAAU,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,aAAa,GAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,MAAM,EACN,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACrB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  // Pure chat-core barrel. No React, no Firebase (client or admin) reachable from
2
2
  // here — safe for Cloud Functions, scripts, and future native/TV clients that
3
- // only need the parser, contracts, and grouping logic.
3
+ // only need the contracts and grouping logic. Zero @ttt-productions/* deps.
4
4
  //
5
5
  // The chat React UI, hooks, Firestore-client adapter config, and render types
6
- // live in @ttt-productions/chat-react. Pure chat schemas live in
7
- // @ttt-productions/chat-schemas.
8
- export { MAX_CHAT_MESSAGE_LENGTH } from "./constants.js";
6
+ // live in @ttt-productions/chat-react. The realtime wire contract and the pure
7
+ // chat Zod schemas live in @ttt-productions/chat-schemas.
9
8
  export { GROUP_GAP_SEC } from "./types.js";
10
9
  export { isContinuation } from "./grouping.js";
11
- export { parseMentionTokens, formatMentionToken } from "./mentions/parser.js";
12
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8EAA8E;AAC9E,uDAAuD;AACvD,EAAE;AACF,8EAA8E;AAC9E,iEAAiE;AACjE,iCAAiC;AAEjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAkB/C,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC","sourcesContent":["// Pure chat-core barrel. No React, no Firebase (client or admin) reachable from\n// here — safe for Cloud Functions, scripts, and future native/TV clients that\n// only need the parser, contracts, and grouping logic.\n//\n// The chat React UI, hooks, Firestore-client adapter config, and render types\n// live in @ttt-productions/chat-react. Pure chat schemas live in\n// @ttt-productions/chat-schemas.\n\nexport { MAX_CHAT_MESSAGE_LENGTH } from \"./constants.js\";\n\nexport { GROUP_GAP_SEC } from \"./types.js\";\nexport { isContinuation } from \"./grouping.js\";\nexport type {\n ChatId,\n ChatThreadV1,\n ChatMessageV1,\n ChatAccessMode,\n ModerationHandlers,\n ChatNameResolver,\n ChatPrewarmSenders,\n} from \"./types.js\";\n\nexport type {\n MentionRef,\n ParsedSegment,\n MentionProvider,\n RecentMentionsAdapter,\n MentionAnchor,\n} from \"./mentions/types.js\";\nexport { parseMentionTokens, formatMentionToken } from \"./mentions/parser.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8EAA8E;AAC9E,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,0DAA0D;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// Pure chat-core barrel. No React, no Firebase (client or admin) reachable from\n// here — safe for Cloud Functions, scripts, and future native/TV clients that\n// only need the contracts and grouping logic. Zero @ttt-productions/* deps.\n//\n// The chat React UI, hooks, Firestore-client adapter config, and render types\n// live in @ttt-productions/chat-react. The realtime wire contract and the pure\n// chat Zod schemas live in @ttt-productions/chat-schemas.\n\nexport { GROUP_GAP_SEC } from \"./types.js\";\nexport { isContinuation } from \"./grouping.js\";\nexport type {\n ChatId,\n ChatThreadV1,\n ChatMessageV1,\n ModerationHandlers,\n ChatNameResolver,\n ChatNameResolution,\n ChatPrewarmSenders,\n} from \"./types.js\";\n"]}
package/dist/types.d.ts CHANGED
@@ -1,7 +1,5 @@
1
- import type { ReplyTo } from "@ttt-productions/chat-schemas";
2
1
  export type ChatId = string;
3
2
  export type ChatThreadV1 = {
4
- allowedUserIds: string[];
5
3
  participantUserIds?: string[];
6
4
  createdAt: number;
7
5
  lastMessageAt: number;
@@ -15,29 +13,17 @@ export type ChatMessageV1 = {
15
13
  senderId: string;
16
14
  text?: string;
17
15
  type?: string;
18
- replyTo?: ReplyTo;
19
16
  isSystemMessage?: boolean;
17
+ /**
18
+ * Account ids a server-written message's text refers to, stored beside the text (never inside
19
+ * it). The UI prewarms and resolves them like senders; an account's anonymization rewrites them.
20
+ */
21
+ referencedUids?: string[];
20
22
  /** Moderation tombstone flag on the stored message (backend-written); consumers
21
23
  * render a tombstone instead of the content when true. */
22
24
  hidden?: boolean;
23
25
  meta?: Record<string, unknown>;
24
26
  };
25
- /**
26
- * Access mode controls how chat decides whether the current user can
27
- * read/write a thread.
28
- *
29
- * - "firestore-rules" — trust Firestore rules. canAccessThread returns true
30
- * for any signed-in user; if rules deny, onSnapshot will surface
31
- * permission-denied. Use this when access depends on data the client doesn't
32
- * reliably know up-front (entity membership, invite participation, etc.).
33
- *
34
- * - "explicit-allowlist" — access is enforced client-side via
35
- * threadAllowedUserIds. The list is required in this mode. Use this when the
36
- * consumer already knows the participants (admin/support threads).
37
- *
38
- * Admins (`isAdmin: true`) bypass both modes.
39
- */
40
- export type ChatAccessMode = "firestore-rules" | "explicit-allowlist";
41
27
  export type ModerationHandlers = {
42
28
  onReportMessage?: (messageId: string, reason?: string) => void | Promise<void>;
43
29
  onReportThread?: (threadId: string, reason?: string) => void | Promise<void>;
@@ -47,11 +33,29 @@ export type ModerationHandlers = {
47
33
  /** Max seconds between messages to be grouped as continuation */
48
34
  export declare const GROUP_GAP_SEC = 120;
49
35
  /**
50
- * Resolves a senderId to a display name synchronously from app-side cache.
51
- * Returns null if the sender is unknown or the cache hasn't loaded yet —
52
- * the chat UI will render a stable fallback ("User") in that case.
36
+ * What the app knows about a sender's display name right now. `pending` — the read has
37
+ * not answered; `unavailable` — the read answered and there is no person to name;
38
+ * `failed` — the read failed, and `retry` asks again. Only `resolved` carries a name:
39
+ * the chat UI never invents one for the other three, it hands them to the app's
40
+ * renderer (chat-react's `renderUnresolvedName`).
41
+ */
42
+ export type ChatNameResolution = {
43
+ status: 'resolved';
44
+ name: string;
45
+ } | {
46
+ status: 'pending';
47
+ } | {
48
+ status: 'unavailable';
49
+ } | {
50
+ status: 'failed';
51
+ retry: () => void;
52
+ };
53
+ /**
54
+ * Resolves a senderId synchronously from the app's own cache. Called during render,
55
+ * so it reads what the app already holds and never starts a fetch itself — the app
56
+ * fetches through {@link ChatPrewarmSenders}.
53
57
  */
54
- export type ChatNameResolver = (senderId: string) => string | null;
58
+ export type ChatNameResolver = (senderId: string) => ChatNameResolution;
55
59
  /**
56
60
  * Optional pre-warm callback. The chat UI calls this with the deduped list of
57
61
  * senderIds visible in the current message page so the consuming app can
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAU7D,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;8DAC0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,cAAc,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAMtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAMF,iEAAiE;AACjE,eAAO,MAAM,aAAa,MAAM,CAAC;AAMjC;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,YAAY,GAAG;IACzB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;8DAC0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAMF,iEAAiE;AACjE,eAAO,MAAM,aAAa,MAAM,CAAC;AAMjC;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,GACzB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAE5C;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,CAAC;AAExE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC"}
package/dist/types.js CHANGED
@@ -1,4 +1,15 @@
1
1
  // ============================================
2
+ // THREAD & MESSAGE
3
+ // ============================================
4
+ //
5
+ // Chat is TEXT-ONLY. A message never carries a file: files belong to the
6
+ // CONVERSATION (the Conversation Files list owned by the consuming app), not to
7
+ // a message in its timeline. There is no attachment contract in this package.
8
+ //
9
+ // A message also carries NO reply pointer: no chat surface has an authoring
10
+ // affordance for replying to a specific message, so a `replyTo` contract here
11
+ // would be unreachable machinery (DJ ruling 2026-07-29).
12
+ // ============================================
2
13
  // MESSAGE GROUPING (internal, exported for tests)
3
14
  // ============================================
4
15
  /** Max seconds between messages to be grouped as continuation */
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAoEA,+CAA+C;AAC/C,kDAAkD;AAClD,+CAA+C;AAE/C,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC","sourcesContent":["import type { ReplyTo } from \"@ttt-productions/chat-schemas\";\n\n// ============================================\n// THREAD & MESSAGE\n// ============================================\n//\n// Chat is TEXT-ONLY. A message never carries a file: files belong to the\n// CONVERSATION (the Conversation Files list owned by the consuming app), not to\n// a message in its timeline. There is no attachment contract in this package.\n\nexport type ChatId = string;\n\nexport type ChatThreadV1 = {\n allowedUserIds: string[];\n participantUserIds?: string[];\n createdAt: number; // millis\n lastMessageAt: number; // millis\n status?: string; // opaque to chat-core\n meta?: Record<string, unknown>; // opaque\n};\n\nexport type ChatMessageV1 = {\n messageId: string;\n threadId: string;\n createdAt: number; // millis\n senderId: string;\n text?: string;\n type?: string; // optional for renderer registry\n replyTo?: ReplyTo;\n isSystemMessage?: boolean;\n /** Moderation tombstone flag on the stored message (backend-written); consumers\n * render a tombstone instead of the content when true. */\n hidden?: boolean;\n meta?: Record<string, unknown>;\n};\n\n// ============================================\n// ACCESS\n// ============================================\n\n/**\n * Access mode controls how chat decides whether the current user can\n * read/write a thread.\n *\n * - \"firestore-rules\" — trust Firestore rules. canAccessThread returns true\n * for any signed-in user; if rules deny, onSnapshot will surface\n * permission-denied. Use this when access depends on data the client doesn't\n * reliably know up-front (entity membership, invite participation, etc.).\n *\n * - \"explicit-allowlist\" — access is enforced client-side via\n * threadAllowedUserIds. The list is required in this mode. Use this when the\n * consumer already knows the participants (admin/support threads).\n *\n * Admins (`isAdmin: true`) bypass both modes.\n */\nexport type ChatAccessMode = \"firestore-rules\" | \"explicit-allowlist\";\n\n// ============================================\n// MODERATION\n// ============================================\n\nexport type ModerationHandlers = {\n onReportMessage?: (messageId: string, reason?: string) => void | Promise<void>;\n onReportThread?: (threadId: string, reason?: string) => void | Promise<void>;\n onDeleteMessage?: (messageId: string) => void | Promise<void>; // admin only (gated)\n onDeleteThread?: (threadId: string) => void | Promise<void>; // admin only (gated)\n};\n\n// ============================================\n// MESSAGE GROUPING (internal, exported for tests)\n// ============================================\n\n/** Max seconds between messages to be grouped as continuation */\nexport const GROUP_GAP_SEC = 120;\n\n// ============================================\n// NAME RESOLUTION\n// ============================================\n\n/**\n * Resolves a senderId to a display name synchronously from app-side cache.\n * Returns null if the sender is unknown or the cache hasn't loaded yet —\n * the chat UI will render a stable fallback (\"User\") in that case.\n */\nexport type ChatNameResolver = (senderId: string) => string | null;\n\n/**\n * Optional pre-warm callback. The chat UI calls this with the deduped list of\n * senderIds visible in the current message page so the consuming app can\n * batch-fetch names into its cache. Implementations should be idempotent —\n * the same id list will be passed across re-renders.\n */\nexport type ChatPrewarmSenders = (senderIds: string[]) => void;\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,mBAAmB;AACnB,+CAA+C;AAC/C,EAAE;AACF,yEAAyE;AACzE,gFAAgF;AAChF,8EAA8E;AAC9E,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,yDAAyD;AA0CzD,+CAA+C;AAC/C,kDAAkD;AAClD,+CAA+C;AAE/C,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC","sourcesContent":["// ============================================\n// THREAD & MESSAGE\n// ============================================\n//\n// Chat is TEXT-ONLY. A message never carries a file: files belong to the\n// CONVERSATION (the Conversation Files list owned by the consuming app), not to\n// a message in its timeline. There is no attachment contract in this package.\n//\n// A message also carries NO reply pointer: no chat surface has an authoring\n// affordance for replying to a specific message, so a `replyTo` contract here\n// would be unreachable machinery (DJ ruling 2026-07-29).\n\nexport type ChatId = string;\n\nexport type ChatThreadV1 = {\n participantUserIds?: string[];\n createdAt: number; // millis\n lastMessageAt: number; // millis\n status?: string; // opaque to chat-core\n meta?: Record<string, unknown>; // opaque\n};\n\nexport type ChatMessageV1 = {\n messageId: string;\n threadId: string;\n createdAt: number; // millis\n senderId: string;\n text?: string;\n type?: string; // optional for renderer registry\n isSystemMessage?: boolean;\n /**\n * Account ids a server-written message's text refers to, stored beside the text (never inside\n * it). The UI prewarms and resolves them like senders; an account's anonymization rewrites them.\n */\n referencedUids?: string[];\n /** Moderation tombstone flag on the stored message (backend-written); consumers\n * render a tombstone instead of the content when true. */\n hidden?: boolean;\n meta?: Record<string, unknown>;\n};\n\n// ============================================\n// MODERATION\n// ============================================\n\nexport type ModerationHandlers = {\n onReportMessage?: (messageId: string, reason?: string) => void | Promise<void>;\n onReportThread?: (threadId: string, reason?: string) => void | Promise<void>;\n onDeleteMessage?: (messageId: string) => void | Promise<void>; // admin only (gated)\n onDeleteThread?: (threadId: string) => void | Promise<void>; // admin only (gated)\n};\n\n// ============================================\n// MESSAGE GROUPING (internal, exported for tests)\n// ============================================\n\n/** Max seconds between messages to be grouped as continuation */\nexport const GROUP_GAP_SEC = 120;\n\n// ============================================\n// NAME RESOLUTION\n// ============================================\n\n/**\n * What the app knows about a sender's display name right now. `pending` — the read has\n * not answered; `unavailable` — the read answered and there is no person to name;\n * `failed` — the read failed, and `retry` asks again. Only `resolved` carries a name:\n * the chat UI never invents one for the other three, it hands them to the app's\n * renderer (chat-react's `renderUnresolvedName`).\n */\nexport type ChatNameResolution =\n | { status: 'resolved'; name: string }\n | { status: 'pending' }\n | { status: 'unavailable' }\n | { status: 'failed'; retry: () => void };\n\n/**\n * Resolves a senderId synchronously from the app's own cache. Called during render,\n * so it reads what the app already holds and never starts a fetch itself — the app\n * fetches through {@link ChatPrewarmSenders}.\n */\nexport type ChatNameResolver = (senderId: string) => ChatNameResolution;\n\n/**\n * Optional pre-warm callback. The chat UI calls this with the deduped list of\n * senderIds visible in the current message page so the consuming app can\n * batch-fetch names into its cache. Implementations should be idempotent —\n * the same id list will be passed across re-renders.\n */\nexport type ChatPrewarmSenders = (senderIds: string[]) => void;\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ttt-productions/chat-core",
3
- "version": "0.11.0",
4
- "description": "Pure chat contracts, mention parser/serializer, and grouping helpers for TTT Productions apps (no React, no Firebase)",
3
+ "version": "0.11.2",
4
+ "description": "Pure chat contracts and grouping helpers for TTT Productions apps (no React, no Firebase)",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",
@@ -27,12 +27,6 @@
27
27
  "typecheck": "tsc --noEmit",
28
28
  "prepublishOnly": "npm run clean && npm run build"
29
29
  },
30
- "dependencies": {
31
- "@ttt-productions/chat-schemas": "^0.10.0"
32
- },
33
- "devDependencies": {
34
- "typescript": "^5.8.3"
35
- },
36
30
  "author": "DJ (TTT Productions)",
37
31
  "license": "MIT"
38
32
  }
@@ -1,7 +0,0 @@
1
- /**
2
- * Maximum chat message text length (characters). Consumer apps may override
3
- * by passing a custom value through `ChatCoreConfig`; this constant is the
4
- * package default and is also what chat-core's own internal schemas use.
5
- */
6
- export declare const MAX_CHAT_MESSAGE_LENGTH = 4000;
7
- //# sourceMappingURL=constants.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC"}
package/dist/constants.js DELETED
@@ -1,8 +0,0 @@
1
- // Chat-domain limits and operational constants.
2
- /**
3
- * Maximum chat message text length (characters). Consumer apps may override
4
- * by passing a custom value through `ChatCoreConfig`; this constant is the
5
- * package default and is also what chat-core's own internal schemas use.
6
- */
7
- export const MAX_CHAT_MESSAGE_LENGTH = 4000;
8
- //# sourceMappingURL=constants.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC","sourcesContent":["// Chat-domain limits and operational constants.\n\n/**\n * Maximum chat message text length (characters). Consumer apps may override\n * by passing a custom value through `ChatCoreConfig`; this constant is the\n * package default and is also what chat-core's own internal schemas use.\n */\nexport const MAX_CHAT_MESSAGE_LENGTH = 4000;\n"]}
@@ -1,33 +0,0 @@
1
- /**
2
- * Mention token parser.
3
- *
4
- * Token grammar:
5
- * @[kind:id] — minimal form; displayText defaults to id
6
- * @[kind:id|displayText] — full form
7
- *
8
- * kind : [A-Za-z0-9-]+
9
- * id : [^\]|]+
10
- * displayText : [^\]]* (may be empty; defaults to id when omitted)
11
- *
12
- * Anything not matching the grammar is preserved verbatim as text segments.
13
- * Malformed tokens (unterminated, illegal characters) are NOT parsed — they
14
- * pass through as plain text so the user always sees what they typed.
15
- */
16
- import type { MentionRef, ParsedSegment } from './types.js';
17
- /**
18
- * Parse a string of message text into an ordered array of text and mention
19
- * segments. Always returns at least one segment for non-empty input. For an
20
- * empty string, returns an empty array.
21
- *
22
- * The parser is intentionally permissive in `TKind` — it returns mentions
23
- * with `kind` typed as `TKind`, but does NOT validate that the parsed kind
24
- * is a member of the consumer's union. Validation, if needed, is the
25
- * consumer's job; the wire token is the source of truth.
26
- */
27
- export declare function parseMentionTokens<TKind extends string = string>(text: string): Array<ParsedSegment<TKind>>;
28
- /**
29
- * Build a wire token from a MentionRef. Inverse of the parser's per-token
30
- * branch — same grammar.
31
- */
32
- export declare function formatMentionToken<TKind extends string>(ref: MentionRef<TKind>): string;
33
- //# sourceMappingURL=parser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/mentions/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAI5D;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAC9D,IAAI,EAAE,MAAM,GACX,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAgC7B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,MAAM,CAEvF"}
@@ -1,61 +0,0 @@
1
- /**
2
- * Mention token parser.
3
- *
4
- * Token grammar:
5
- * @[kind:id] — minimal form; displayText defaults to id
6
- * @[kind:id|displayText] — full form
7
- *
8
- * kind : [A-Za-z0-9-]+
9
- * id : [^\]|]+
10
- * displayText : [^\]]* (may be empty; defaults to id when omitted)
11
- *
12
- * Anything not matching the grammar is preserved verbatim as text segments.
13
- * Malformed tokens (unterminated, illegal characters) are NOT parsed — they
14
- * pass through as plain text so the user always sees what they typed.
15
- */
16
- const TOKEN_RE = /@\[([A-Za-z0-9-]+):([^\]|]+)(?:\|([^\]]*))?\]/g;
17
- /**
18
- * Parse a string of message text into an ordered array of text and mention
19
- * segments. Always returns at least one segment for non-empty input. For an
20
- * empty string, returns an empty array.
21
- *
22
- * The parser is intentionally permissive in `TKind` — it returns mentions
23
- * with `kind` typed as `TKind`, but does NOT validate that the parsed kind
24
- * is a member of the consumer's union. Validation, if needed, is the
25
- * consumer's job; the wire token is the source of truth.
26
- */
27
- export function parseMentionTokens(text) {
28
- if (text.length === 0)
29
- return [];
30
- const segments = [];
31
- let lastIndex = 0;
32
- TOKEN_RE.lastIndex = 0;
33
- let match;
34
- while ((match = TOKEN_RE.exec(text)) !== null) {
35
- const [whole, kindRaw, idRaw, displayRaw] = match;
36
- const start = match.index;
37
- const end = start + whole.length;
38
- if (start > lastIndex) {
39
- segments.push({ type: 'text', text: text.slice(lastIndex, start) });
40
- }
41
- const ref = {
42
- kind: kindRaw,
43
- id: idRaw,
44
- displayText: displayRaw && displayRaw.length > 0 ? displayRaw : idRaw,
45
- };
46
- segments.push({ type: 'mention', ref });
47
- lastIndex = end;
48
- }
49
- if (lastIndex < text.length) {
50
- segments.push({ type: 'text', text: text.slice(lastIndex) });
51
- }
52
- return segments;
53
- }
54
- /**
55
- * Build a wire token from a MentionRef. Inverse of the parser's per-token
56
- * branch — same grammar.
57
- */
58
- export function formatMentionToken(ref) {
59
- return `@[${ref.kind}:${ref.id}|${ref.displayText}]`;
60
- }
61
- //# sourceMappingURL=parser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/mentions/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,QAAQ,GAAG,gDAAgD,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY;IAEZ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjC,MAAM,QAAQ,GAAgC,EAAE,CAAC;IACjD,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,QAAQ,CAAC,SAAS,GAAG,CAAC,CAAC;IACvB,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9C,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC;QAClD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAC1B,MAAM,GAAG,GAAG,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;QAEjC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,MAAM,GAAG,GAAsB;YAC7B,IAAI,EAAE,OAAgB;YACtB,EAAE,EAAE,KAAK;YACT,WAAW,EAAE,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK;SACtE,CAAC;QACF,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAExC,SAAS,GAAG,GAAG,CAAC;IAClB,CAAC;IAED,IAAI,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAuB,GAAsB;IAC7E,OAAO,KAAK,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,WAAW,GAAG,CAAC;AACvD,CAAC","sourcesContent":["/**\n * Mention token parser.\n *\n * Token grammar:\n * @[kind:id] — minimal form; displayText defaults to id\n * @[kind:id|displayText] — full form\n *\n * kind : [A-Za-z0-9-]+\n * id : [^\\]|]+\n * displayText : [^\\]]* (may be empty; defaults to id when omitted)\n *\n * Anything not matching the grammar is preserved verbatim as text segments.\n * Malformed tokens (unterminated, illegal characters) are NOT parsed — they\n * pass through as plain text so the user always sees what they typed.\n */\n\nimport type { MentionRef, ParsedSegment } from './types.js';\n\nconst TOKEN_RE = /@\\[([A-Za-z0-9-]+):([^\\]|]+)(?:\\|([^\\]]*))?\\]/g;\n\n/**\n * Parse a string of message text into an ordered array of text and mention\n * segments. Always returns at least one segment for non-empty input. For an\n * empty string, returns an empty array.\n *\n * The parser is intentionally permissive in `TKind` — it returns mentions\n * with `kind` typed as `TKind`, but does NOT validate that the parsed kind\n * is a member of the consumer's union. Validation, if needed, is the\n * consumer's job; the wire token is the source of truth.\n */\nexport function parseMentionTokens<TKind extends string = string>(\n text: string,\n): Array<ParsedSegment<TKind>> {\n if (text.length === 0) return [];\n\n const segments: Array<ParsedSegment<TKind>> = [];\n let lastIndex = 0;\n\n TOKEN_RE.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = TOKEN_RE.exec(text)) !== null) {\n const [whole, kindRaw, idRaw, displayRaw] = match;\n const start = match.index;\n const end = start + whole.length;\n\n if (start > lastIndex) {\n segments.push({ type: 'text', text: text.slice(lastIndex, start) });\n }\n\n const ref: MentionRef<TKind> = {\n kind: kindRaw as TKind,\n id: idRaw,\n displayText: displayRaw && displayRaw.length > 0 ? displayRaw : idRaw,\n };\n segments.push({ type: 'mention', ref });\n\n lastIndex = end;\n }\n\n if (lastIndex < text.length) {\n segments.push({ type: 'text', text: text.slice(lastIndex) });\n }\n\n return segments;\n}\n\n/**\n * Build a wire token from a MentionRef. Inverse of the parser's per-token\n * branch — same grammar.\n */\nexport function formatMentionToken<TKind extends string>(ref: MentionRef<TKind>): string {\n return `@[${ref.kind}:${ref.id}|${ref.displayText}]`;\n}\n"]}
@@ -1,94 +0,0 @@
1
- /**
2
- * Generic mention system types.
3
- *
4
- * chat-core owns the mention infrastructure (parsing, autocomplete UI, keyboard
5
- * behavior, composer insertion, message text rendering) but knows nothing about
6
- * what a "user" or "entity" is. The consumer parameterizes `TKind` to its own
7
- * union and supplies one `MentionProvider` per kind.
8
- *
9
- * Wire format — mentions are encoded inline in the message's `text` field as
10
- * `@[kind:id|displayText]` tokens. The renderer parses them out at display
11
- * time. No schema change to ChatMessage.
12
- *
13
- * These are the PURE mention contracts. The React-coupled render type for
14
- * autocomplete rows (`renderResult`) lives in @ttt-productions/chat-react.
15
- */
16
- /**
17
- * Resolved mention — the value that ends up in the wire token after the user
18
- * selects an autocomplete result.
19
- *
20
- * `kind` is the discriminator supplied by the consumer (e.g. `'member'`, `'tag'`); `id` is the
21
- * consumer-chosen stable identifier; `displayText` is the user-facing label
22
- * (typically a display name or title).
23
- */
24
- export type MentionRef<TKind extends string = string> = {
25
- kind: TKind;
26
- id: string;
27
- displayText: string;
28
- };
29
- /**
30
- * Single segment in parsed message text. Either a literal text run or a
31
- * resolved mention. The renderer walks an ordered array of these.
32
- */
33
- export type ParsedSegment<TKind extends string = string> = {
34
- type: 'text';
35
- text: string;
36
- } | {
37
- type: 'mention';
38
- ref: MentionRef<TKind>;
39
- };
40
- /**
41
- * Provider supplying autocomplete results for one mention kind.
42
- *
43
- * - `kind` — the discriminator. Must match `MentionRef.kind` for results
44
- * returned by `search`.
45
- * - `label` — human-readable label for the section / tab.
46
- * - `search` — async query. Receives the current query string (after the
47
- * trigger character), the consumer-supplied `context`, and an optional
48
- * `signal` for cancellation. Returns up to whatever limit the provider
49
- * chooses; the chat UI renders all returned results.
50
- *
51
- * `TContext` is consumer-owned — pass anything you need inside `search`
52
- * (Firestore db, current user id, surface-specific filters). The mention
53
- * system never inspects it.
54
- *
55
- * Optional per-row custom rendering (`renderResult`) is a React concern and is
56
- * layered on top of this pure provider by `RenderableMentionProvider` in
57
- * @ttt-productions/chat-react.
58
- */
59
- export type MentionProvider<TKind extends string = string, TContext = unknown> = {
60
- kind: TKind;
61
- label: string;
62
- search: (args: {
63
- query: string;
64
- context: TContext;
65
- signal?: AbortSignal;
66
- }) => Promise<MentionRef<TKind>[]>;
67
- };
68
- /**
69
- * Optional recent-mentions adapter. When provided, the autocomplete UI shows
70
- * recent picks when the user opens the dropdown without typing a query
71
- * (or after typing fewer than the configured minimum characters).
72
- *
73
- * `getRecent` MAY be async — the autocomplete renders a spinner while it
74
- * resolves. `recordUse` is fired after a successful selection so consumers
75
- * can persist usage.
76
- */
77
- export type RecentMentionsAdapter<TKind extends string = string> = {
78
- getRecent: () => MentionRef<TKind>[] | Promise<MentionRef<TKind>[]>;
79
- recordUse?: (ref: MentionRef<TKind>) => void | Promise<void>;
80
- };
81
- /**
82
- * Anchor describing the position of an inserted mention inside the composer's
83
- * text buffer. Tracked in component state so positionally-collocated mentions
84
- * with the same display string disambiguate on send.
85
- *
86
- * - `start` / `end` — character offsets into the current textarea value.
87
- * - `ref` — the resolved MentionRef.
88
- */
89
- export type MentionAnchor<TKind extends string = string> = {
90
- start: number;
91
- end: number;
92
- ref: MentionRef<TKind>;
93
- };
94
- //# sourceMappingURL=types.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/mentions/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,IAAI;IACtD,IAAI,EAAE,KAAK,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,IACnD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,CAAA;CAAE,CAAC;AAEhD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;IAC/E,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,CAAC,IAAI,EAAE;QACb,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,QAAQ,CAAC;QAClB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;CACpC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,IAAI;IACjE,SAAS,EAAE,MAAM,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,IAAI;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;CACxB,CAAC"}
@@ -1,17 +0,0 @@
1
- /**
2
- * Generic mention system types.
3
- *
4
- * chat-core owns the mention infrastructure (parsing, autocomplete UI, keyboard
5
- * behavior, composer insertion, message text rendering) but knows nothing about
6
- * what a "user" or "entity" is. The consumer parameterizes `TKind` to its own
7
- * union and supplies one `MentionProvider` per kind.
8
- *
9
- * Wire format — mentions are encoded inline in the message's `text` field as
10
- * `@[kind:id|displayText]` tokens. The renderer parses them out at display
11
- * time. No schema change to ChatMessage.
12
- *
13
- * These are the PURE mention contracts. The React-coupled render type for
14
- * autocomplete rows (`renderResult`) lives in @ttt-productions/chat-react.
15
- */
16
- export {};
17
- //# sourceMappingURL=types.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/mentions/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG","sourcesContent":["/**\n * Generic mention system types.\n *\n * chat-core owns the mention infrastructure (parsing, autocomplete UI, keyboard\n * behavior, composer insertion, message text rendering) but knows nothing about\n * what a \"user\" or \"entity\" is. The consumer parameterizes `TKind` to its own\n * union and supplies one `MentionProvider` per kind.\n *\n * Wire format — mentions are encoded inline in the message's `text` field as\n * `@[kind:id|displayText]` tokens. The renderer parses them out at display\n * time. No schema change to ChatMessage.\n *\n * These are the PURE mention contracts. The React-coupled render type for\n * autocomplete rows (`renderResult`) lives in @ttt-productions/chat-react.\n */\n\n/**\n * Resolved mention — the value that ends up in the wire token after the user\n * selects an autocomplete result.\n *\n * `kind` is the discriminator supplied by the consumer (e.g. `'member'`, `'tag'`); `id` is the\n * consumer-chosen stable identifier; `displayText` is the user-facing label\n * (typically a display name or title).\n */\nexport type MentionRef<TKind extends string = string> = {\n kind: TKind;\n id: string;\n displayText: string;\n};\n\n/**\n * Single segment in parsed message text. Either a literal text run or a\n * resolved mention. The renderer walks an ordered array of these.\n */\nexport type ParsedSegment<TKind extends string = string> =\n | { type: 'text'; text: string }\n | { type: 'mention'; ref: MentionRef<TKind> };\n\n/**\n * Provider supplying autocomplete results for one mention kind.\n *\n * - `kind` — the discriminator. Must match `MentionRef.kind` for results\n * returned by `search`.\n * - `label` — human-readable label for the section / tab.\n * - `search` — async query. Receives the current query string (after the\n * trigger character), the consumer-supplied `context`, and an optional\n * `signal` for cancellation. Returns up to whatever limit the provider\n * chooses; the chat UI renders all returned results.\n *\n * `TContext` is consumer-owned — pass anything you need inside `search`\n * (Firestore db, current user id, surface-specific filters). The mention\n * system never inspects it.\n *\n * Optional per-row custom rendering (`renderResult`) is a React concern and is\n * layered on top of this pure provider by `RenderableMentionProvider` in\n * @ttt-productions/chat-react.\n */\nexport type MentionProvider<TKind extends string = string, TContext = unknown> = {\n kind: TKind;\n label: string;\n search: (args: {\n query: string;\n context: TContext;\n signal?: AbortSignal;\n }) => Promise<MentionRef<TKind>[]>;\n};\n\n/**\n * Optional recent-mentions adapter. When provided, the autocomplete UI shows\n * recent picks when the user opens the dropdown without typing a query\n * (or after typing fewer than the configured minimum characters).\n *\n * `getRecent` MAY be async — the autocomplete renders a spinner while it\n * resolves. `recordUse` is fired after a successful selection so consumers\n * can persist usage.\n */\nexport type RecentMentionsAdapter<TKind extends string = string> = {\n getRecent: () => MentionRef<TKind>[] | Promise<MentionRef<TKind>[]>;\n recordUse?: (ref: MentionRef<TKind>) => void | Promise<void>;\n};\n\n/**\n * Anchor describing the position of an inserted mention inside the composer's\n * text buffer. Tracked in component state so positionally-collocated mentions\n * with the same display string disambiguate on send.\n *\n * - `start` / `end` — character offsets into the current textarea value.\n * - `ref` — the resolved MentionRef.\n */\nexport type MentionAnchor<TKind extends string = string> = {\n start: number;\n end: number;\n ref: MentionRef<TKind>;\n};\n"]}