@pmate/chat-sdk 0.12.0-next.7 → 0.12.0-next.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +170 -81
  2. package/lib/advanced.d.ts +10 -0
  3. package/lib/advanced.js +10 -0
  4. package/lib/chat.d.ts +5 -0
  5. package/lib/chat.js +11 -0
  6. package/lib/chatContract.d.ts +148 -0
  7. package/lib/chatContract.js +1 -0
  8. package/lib/chatError.d.ts +11 -0
  9. package/lib/chatError.js +13 -0
  10. package/lib/chatTypes.d.ts +121 -0
  11. package/lib/chatTypes.js +1 -0
  12. package/lib/commands.d.ts +20 -0
  13. package/lib/commands.js +12 -0
  14. package/lib/components/ChatInputDesktop.js +5 -4
  15. package/lib/components/ChatInputMobile.js +5 -4
  16. package/lib/components/CommandMenu.d.ts +15 -0
  17. package/lib/components/CommandMenu.js +10 -0
  18. package/lib/credentials.d.ts +26 -0
  19. package/lib/credentials.js +110 -0
  20. package/lib/fileMetadata.d.ts +9 -0
  21. package/lib/fileMetadata.js +18 -0
  22. package/lib/framework.d.ts +11 -0
  23. package/lib/framework.js +12 -0
  24. package/lib/history.d.ts +4 -0
  25. package/lib/history.js +8 -0
  26. package/lib/hooks/useChatSnapshot.d.ts +4 -0
  27. package/lib/hooks/useChatSnapshot.js +9 -0
  28. package/lib/hooks/useComposerSuggestions.d.ts +50 -0
  29. package/lib/hooks/useComposerSuggestions.js +70 -0
  30. package/lib/index.d.ts +5 -0
  31. package/lib/index.js +5 -0
  32. package/lib/internal/chatEngine.d.ts +131 -0
  33. package/lib/internal/chatEngine.js +280 -0
  34. package/lib/internal/chatRegistry.d.ts +4 -0
  35. package/lib/internal/chatRegistry.js +4 -0
  36. package/lib/internal/createPublicChat.d.ts +4 -0
  37. package/lib/internal/createPublicChat.js +52 -0
  38. package/lib/messageView.d.ts +6 -0
  39. package/lib/messageView.js +103 -0
  40. package/lib/runtime/batchedReadIndex.d.ts +16 -3
  41. package/lib/runtime/batchedReadIndex.js +50 -4
  42. package/lib/runtime/cachedMessage.d.ts +12 -0
  43. package/lib/runtime/cachedMessage.js +18 -0
  44. package/lib/runtime/chatClient.d.ts +2 -0
  45. package/lib/runtime/chatClient.js +8 -3
  46. package/lib/runtime/chatError.d.ts +3 -0
  47. package/lib/runtime/chatError.js +15 -0
  48. package/lib/runtime/chatSession.d.ts +127 -13
  49. package/lib/runtime/chatSession.js +727 -40
  50. package/lib/runtime/connectivity.d.ts +4 -0
  51. package/lib/runtime/connectivity.js +8 -0
  52. package/lib/runtime/conversationDirectory.d.ts +37 -0
  53. package/lib/runtime/conversationDirectory.js +116 -0
  54. package/lib/runtime/conversationRead.d.ts +13 -0
  55. package/lib/runtime/conversationRead.js +34 -0
  56. package/lib/runtime/conversationReading.d.ts +28 -0
  57. package/lib/runtime/conversationReading.js +47 -0
  58. package/lib/runtime/createChat.d.ts +11 -7
  59. package/lib/runtime/createChat.js +46 -23
  60. package/lib/runtime/durableSender.d.ts +97 -14
  61. package/lib/runtime/durableSender.js +786 -71
  62. package/lib/runtime/groupClient.d.ts +76 -0
  63. package/lib/runtime/groupClient.js +325 -0
  64. package/lib/runtime/index.d.ts +7 -0
  65. package/lib/runtime/index.js +5 -0
  66. package/lib/runtime/messageClient.d.ts +35 -8
  67. package/lib/runtime/messageClient.js +364 -66
  68. package/lib/runtime/messageControls.d.ts +19 -0
  69. package/lib/runtime/messageControls.js +44 -0
  70. package/lib/runtime/messageLaunch.d.ts +44 -0
  71. package/lib/runtime/messageLaunch.js +137 -0
  72. package/lib/runtime/messageQuotes.d.ts +3 -0
  73. package/lib/runtime/messageQuotes.js +17 -0
  74. package/lib/runtime/messageReaders.d.ts +12 -1
  75. package/lib/runtime/messageReaders.js +67 -3
  76. package/lib/runtime/messageStore.d.ts +40 -2
  77. package/lib/runtime/messageStore.js +227 -11
  78. package/lib/runtime/messageTimeline.d.ts +50 -0
  79. package/lib/runtime/messageTimeline.js +120 -0
  80. package/lib/runtime/outboxSchema.d.ts +5 -0
  81. package/lib/runtime/outboxSchema.js +59 -0
  82. package/lib/runtime/outgoingContent.d.ts +89 -0
  83. package/lib/runtime/outgoingContent.js +113 -0
  84. package/lib/runtime/readBatchJournal.d.ts +35 -0
  85. package/lib/runtime/readBatchJournal.js +210 -0
  86. package/lib/runtime/readCounts.d.ts +15 -1
  87. package/lib/runtime/readCounts.js +97 -9
  88. package/lib/runtime/readIndex.d.ts +68 -7
  89. package/lib/runtime/readIndex.js +460 -22
  90. package/lib/runtime/readSchema.d.ts +2 -0
  91. package/lib/runtime/readSchema.js +58 -0
  92. package/lib/runtime/readSync.d.ts +1 -0
  93. package/lib/runtime/readSync.js +26 -9
  94. package/lib/runtime/readingClient.d.ts +19 -0
  95. package/lib/runtime/readingClient.js +131 -19
  96. package/lib/runtime/readingContent.d.ts +5 -0
  97. package/lib/runtime/readingContent.js +23 -0
  98. package/lib/runtime/responseCache.d.ts +59 -0
  99. package/lib/runtime/responseCache.js +232 -0
  100. package/lib/runtime/socialReadClient.d.ts +4 -2
  101. package/lib/runtime/socialReadClient.js +12 -8
  102. package/lib/runtime/voiceUpload.d.ts +2 -0
  103. package/lib/runtime/voiceUpload.js +27 -0
  104. package/lib/types/chatInput.d.ts +3 -0
  105. package/package.json +27 -2
@@ -0,0 +1,20 @@
1
+ /** Host-owned command catalog. Selecting a suggestion edits the draft only. */
2
+ export type ChatCommand = {
3
+ name: string;
4
+ description: string;
5
+ usage?: string;
6
+ };
7
+ export type ChatCommandConfig = {
8
+ commands: ChatCommand[];
9
+ label?: string;
10
+ emptyText?: string;
11
+ };
12
+ export declare function commandQuery(value: string, caret: number): {
13
+ query: string;
14
+ end: number;
15
+ } | null;
16
+ export declare function filterCommands(commands: ChatCommand[], query: string): ChatCommand[];
17
+ export declare function completeCommand(value: string, end: number, command: ChatCommand): {
18
+ text: string;
19
+ caret: number;
20
+ };
@@ -0,0 +1,12 @@
1
+ export function commandQuery(value, caret) {
2
+ const match = /^\/([^\s/]*)(?=\s|$)/.exec(value);
3
+ return match && caret >= 1 && caret <= match[0].length ? { query: value.slice(1, caret).toLowerCase(), end: match[0].length } : null;
4
+ }
5
+ export function filterCommands(commands, query) {
6
+ return commands.filter(c => /^[a-z][a-z0-9-]*$/.test(c.name) && c.name.toLowerCase().startsWith(query));
7
+ }
8
+ export function completeCommand(value, end, command) {
9
+ const prefix = `/${command.name}`, suffix = value.slice(end);
10
+ const text = prefix + (suffix || " ");
11
+ return { text, caret: prefix.length + 1 };
12
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { handleImagePaste } from "../ux/imagePaste";
3
- import { useMentions } from "../hooks/useMentions";
3
+ import { useComposerSuggestions } from "../hooks/useComposerSuggestions";
4
+ import { CommandMenu } from "./CommandMenu";
4
5
  import { MentionMenu } from "./MentionMenu";
5
6
  import React from "react";
6
7
  import { Provider, createStore } from "jotai";
@@ -25,8 +26,8 @@ function DesktopInner(props) {
25
26
  const tokens = mergeChatInputTokens(props.tokens);
26
27
  const state = useChatInputState();
27
28
  const attachments = useAttachmentState();
28
- const mentions = useMentions({
29
- value: state.text, config: props.mentions, disabled: props.disabled || state.status === "sending" || state.mode === "voice",
29
+ const mentions = useComposerSuggestions({
30
+ value: state.text, mentions: props.mentions, commands: props.commands, disabled: props.disabled || state.status === "sending" || state.mode === "voice",
30
31
  onChange(value) { state.setText(value); state.setStatus(value.trim() ? "typing" : "idle"); props.onChange?.(value); },
31
32
  });
32
33
  const [isEmojiOpen, setIsEmojiOpen] = useState(false);
@@ -186,7 +187,7 @@ function DesktopInner(props) {
186
187
  alignSelf: "center",
187
188
  transition: `color ${tokens.motion.stateTransition}, transform ${tokens.motion.pressFeedback}`,
188
189
  };
189
- return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [_jsx(MentionMenu, { ...mentions.menuProps, tokens: tokens }), _jsxs("div", { className: "grid gap-3", style: { position: "relative", width: "100%" }, children: [_jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
190
+ return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [_jsx(MentionMenu, { ...mentions.menuProps, tokens: tokens }), _jsx(CommandMenu, { ...mentions.commandMenuProps, tokens: tokens }), _jsxs("div", { className: "grid gap-3", style: { position: "relative", width: "100%" }, children: [_jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
190
191
  const file = event.target.files?.[0];
191
192
  if (file) {
192
193
  void handleImageFile(file);
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { handleImagePaste } from "../ux/imagePaste";
3
- import { useMentions } from "../hooks/useMentions";
3
+ import { useComposerSuggestions } from "../hooks/useComposerSuggestions";
4
+ import { CommandMenu } from "./CommandMenu";
4
5
  import { MentionMenu } from "./MentionMenu";
5
6
  import React from "react";
6
7
  import { Provider, createStore } from "jotai";
@@ -26,8 +27,8 @@ function MobileInner(props) {
26
27
  const tokens = mergeChatInputTokens(props.tokens);
27
28
  const state = useChatInputState();
28
29
  const attachments = useAttachmentState();
29
- const mentions = useMentions({
30
- value: state.text, config: props.mentions, disabled: props.disabled || state.status === "sending" || state.mode === "voice",
30
+ const mentions = useComposerSuggestions({
31
+ value: state.text, mentions: props.mentions, commands: props.commands, disabled: props.disabled || state.status === "sending" || state.mode === "voice",
31
32
  onChange(value) { state.setText(value); state.setStatus(value.trim() ? "typing" : "idle"); props.onChange?.(value); },
32
33
  });
33
34
  const [recordingChoice, setRecordingChoice] = useState("send");
@@ -267,7 +268,7 @@ function MobileInner(props) {
267
268
  userSelect: "none",
268
269
  WebkitUserSelect: "none",
269
270
  WebkitTouchCallout: "none",
270
- }, children: [_jsx(MentionMenu, { ...mentions.menuProps, tokens: tokens }), isRecordingOverlayVisible ? (_jsxs("div", { style: {
271
+ }, children: [_jsx(MentionMenu, { ...mentions.menuProps, tokens: tokens }), _jsx(CommandMenu, { ...mentions.commandMenuProps, tokens: tokens }), isRecordingOverlayVisible ? (_jsxs("div", { style: {
271
272
  position: "fixed",
272
273
  left: "50%",
273
274
  bottom: "calc(72px + env(safe-area-inset-bottom, 0px))",
@@ -0,0 +1,15 @@
1
+ import type { RefObject } from "react";
2
+ import type { ChatCommand } from "../commands";
3
+ import type { ChatInputTokens, DeepPartial } from "../types/chatInput";
4
+ export declare function CommandMenu({ id, open, candidates, activeIndex, onSelect, onActiveIndex, menuRef, label, emptyText, tokens: overrides }: {
5
+ id: string;
6
+ open: boolean;
7
+ candidates: ChatCommand[];
8
+ activeIndex: number;
9
+ onSelect: (c: ChatCommand) => void;
10
+ onActiveIndex: (i: number) => void;
11
+ menuRef: RefObject<HTMLDivElement | null>;
12
+ label: string;
13
+ emptyText: string;
14
+ tokens?: DeepPartial<ChatInputTokens>;
15
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,10 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useEffect } from "react";
3
+ import { mergeChatInputTokens } from "../tokens/defaultTokens";
4
+ export function CommandMenu({ id, open, candidates, activeIndex, onSelect, onActiveIndex, menuRef, label, emptyText, tokens: overrides }) {
5
+ const tokens = mergeChatInputTokens(overrides);
6
+ useEffect(() => { menuRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" }); }, [activeIndex, open]);
7
+ if (!open)
8
+ return null;
9
+ return _jsx("div", { id: id, ref: menuRef, role: "listbox", "aria-label": label, style: { position: "absolute", bottom: "calc(100% + 8px)", left: 0, width: "min(420px, 100%)", maxHeight: "min(320px, 40dvh)", overflowY: "auto", overscrollBehavior: "contain", zIndex: 20, boxSizing: "border-box", padding: 6, borderRadius: tokens.radius.inputSurface, border: `1px solid ${tokens.color.borderDefault}`, background: tokens.color.surfaceInputFocus, color: tokens.color.textPrimary, boxShadow: tokens.shadow.inputElevated, fontFamily: tokens.typography.familyBase }, children: candidates.length ? candidates.map((c, i) => _jsxs("button", { id: `${id}-${i}`, type: "button", role: "option", "aria-selected": i === activeIndex, tabIndex: -1, onMouseDown: e => e.preventDefault(), onMouseMove: () => onActiveIndex(i), onClick: () => onSelect(c), style: { display: "block", width: "100%", minHeight: 48, padding: "9px 12px", border: 0, borderRadius: 8, textAlign: "left", cursor: "pointer", color: "inherit", background: i === activeIndex ? `color-mix(in srgb, ${tokens.color.brandPrimary} 14%, ${tokens.color.surfaceInputFocus})` : "transparent" }, children: [_jsxs("span", { style: { display: "block", overflowWrap: "anywhere", fontSize: tokens.typography.inputText.size }, children: [_jsxs("strong", { style: { color: tokens.color.brandPrimary }, children: ["/", c.name] }), c.usage ? _jsx("span", { style: { marginLeft: 8, fontSize: 12, color: tokens.color.textSecondary }, children: c.usage }) : null] }), _jsx("small", { style: { display: "block", marginTop: 3, color: tokens.color.textSecondary }, children: c.description })] }, c.name)) : _jsx("div", { role: "status", style: { padding: 12, color: tokens.color.textSecondary }, children: emptyText }) });
10
+ }
@@ -0,0 +1,26 @@
1
+ export type TokenRequest = Readonly<{
2
+ forceRefresh: boolean;
3
+ signal: AbortSignal;
4
+ }>;
5
+ export type TokenProvider = (request: TokenRequest) => string | Promise<string>;
6
+ /** Cancel the caller's wait, not the underlying shared operation. */
7
+ export declare function waitForCaller<T>(task: Promise<T>, signal?: AbortSignal, onAbandonedValue?: (value: T) => void): Promise<T>;
8
+ /** One scoped credential owner; stale 401s share one refresh rather than cascading. */
9
+ export declare class ChatCredentials {
10
+ private provider;
11
+ private origin;
12
+ private transport;
13
+ private lifetime;
14
+ private flight?;
15
+ private refreshing?;
16
+ private latest?;
17
+ private recovery?;
18
+ private recoveredFrom;
19
+ private version;
20
+ constructor(provider: TokenProvider, origin: string, transport?: typeof fetch);
21
+ private acquire;
22
+ private recover;
23
+ getToken: () => Promise<string>;
24
+ fetch: typeof fetch;
25
+ close(): void;
26
+ }
@@ -0,0 +1,110 @@
1
+ import { ChatError } from "./chatError.js";
2
+ /** Cancel the caller's wait, not the underlying shared operation. */
3
+ export function waitForCaller(task, signal, onAbandonedValue) {
4
+ if (!signal)
5
+ return task;
6
+ if (signal.aborted) {
7
+ void task.then(value => onAbandonedValue?.(value), () => { });
8
+ return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
9
+ }
10
+ return new Promise((resolve, reject) => {
11
+ let settled = false;
12
+ const abort = () => { if (settled)
13
+ return; settled = true; signal.removeEventListener("abort", abort); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); };
14
+ signal.addEventListener("abort", abort, { once: true });
15
+ task.then(value => { signal.removeEventListener("abort", abort); if (settled) {
16
+ onAbandonedValue?.(value);
17
+ return;
18
+ } settled = true; resolve(value); }, error => { signal.removeEventListener("abort", abort); if (!settled) {
19
+ settled = true;
20
+ reject(error);
21
+ } });
22
+ });
23
+ }
24
+ /** One scoped credential owner; stale 401s share one refresh rather than cascading. */
25
+ export class ChatCredentials {
26
+ provider;
27
+ origin;
28
+ transport;
29
+ lifetime = new AbortController();
30
+ flight;
31
+ refreshing;
32
+ latest;
33
+ recovery;
34
+ recoveredFrom = -1;
35
+ version = 0;
36
+ constructor(provider, origin, transport = fetch) {
37
+ this.provider = provider;
38
+ this.origin = origin;
39
+ this.transport = transport;
40
+ }
41
+ acquire(forceRefresh = false) {
42
+ if (this.lifetime.signal.aborted)
43
+ return Promise.reject(new ChatError("closed", "Chat credentials closed"));
44
+ if (this.refreshing)
45
+ return this.refreshing;
46
+ if (!forceRefresh && this.flight)
47
+ return this.flight;
48
+ const version = forceRefresh ? ++this.version : this.version;
49
+ const signal = AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(15000)]);
50
+ const task = waitForCaller(Promise.resolve().then(() => this.provider({ forceRefresh, signal })), signal).then(token => {
51
+ if (this.lifetime.signal.aborted)
52
+ throw new ChatError("closed", "Chat credentials closed");
53
+ if (typeof token !== "string" || !token.trim())
54
+ throw new ChatError("authentication-required", "A valid access token is required");
55
+ if (version < this.version && this.latest)
56
+ return this.latest;
57
+ const value = { token, version };
58
+ this.latest = value;
59
+ return value;
60
+ }).catch(error => {
61
+ if (error?.status === 401)
62
+ throw new ChatError("authentication-required", "Credentials require a fresh login", { cause: error, status: 401 });
63
+ if (error?.status === 403)
64
+ throw new ChatError("access-denied", "Credential access denied", { cause: error, status: 403 });
65
+ throw error;
66
+ }).finally(() => { if (this.flight === task)
67
+ this.flight = undefined; if (this.refreshing === task)
68
+ this.refreshing = undefined; });
69
+ if (forceRefresh)
70
+ this.refreshing = task;
71
+ else
72
+ this.flight = task;
73
+ return task;
74
+ }
75
+ recover(used) {
76
+ if (this.latest && this.latest.version > used.version)
77
+ return Promise.resolve(this.latest);
78
+ if (this.recovery && used.version <= this.recoveredFrom)
79
+ return this.recovery;
80
+ this.recoveredFrom = used.version;
81
+ this.recovery = this.acquire(true);
82
+ return this.recovery;
83
+ }
84
+ getToken = async () => { return (await this.acquire()).token; };
85
+ fetch = async (input, init) => {
86
+ const transport = this.transport;
87
+ const url = new URL(input instanceof Request ? input.url : String(input)), headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
88
+ // Never attach an app credential to uploads, other origins or unauthenticated requests.
89
+ if (url.origin !== this.origin || !headers.has("authorization"))
90
+ return transport(input, init);
91
+ const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
92
+ const signal = AbortSignal.any([this.lifetime.signal, ...(callerSignal ? [callerSignal] : [])]);
93
+ signal?.throwIfAborted();
94
+ const original = input instanceof Request ? input.clone() : input;
95
+ const send = async (token) => {
96
+ signal?.throwIfAborted();
97
+ headers.set("authorization", `Bearer ${token}`);
98
+ return transport(original instanceof Request ? original.clone() : original, { ...init, headers, signal });
99
+ };
100
+ const used = await waitForCaller(this.acquire(), signal ?? undefined);
101
+ const response = await send(used.token);
102
+ if (response.status !== 401)
103
+ return response;
104
+ // Denial (403) is terminal. Only an unauthenticated response gets one replay.
105
+ void response.body?.cancel().catch(() => { });
106
+ const next = await waitForCaller(this.recover(used), signal);
107
+ return send(next.token);
108
+ };
109
+ close() { this.lifetime.abort(new ChatError("closed", "Chat credentials closed")); }
110
+ }
@@ -0,0 +1,9 @@
1
+ /** Durable file reference. Upload limits and download policy belong to the host. */
2
+ export type ChatFile = Readonly<{
3
+ url: string;
4
+ name: string;
5
+ size: number;
6
+ mimeType?: string;
7
+ expiresAt?: number;
8
+ }>;
9
+ export declare function parseChatFile(value: unknown): ChatFile | undefined;
@@ -0,0 +1,18 @@
1
+ export function parseChatFile(value) {
2
+ if (!value || typeof value !== "object")
3
+ return;
4
+ const file = value;
5
+ if (typeof file.name !== "string" || !file.name.trim() || file.name.length > 255 || /[\u0000-\u001f\\/]/u.test(file.name) || typeof file.url !== "string" || file.url.length > 8192 || !Number.isSafeInteger(file.size) || Number(file.size) < 0)
6
+ return;
7
+ try {
8
+ const url = new URL(file.url);
9
+ if (url.protocol !== "https:" || url.username || url.password)
10
+ return;
11
+ }
12
+ catch {
13
+ return;
14
+ }
15
+ return { url: file.url, name: file.name, size: file.size,
16
+ ...(typeof file.mimeType === "string" && file.mimeType.length <= 255 && !/[\r\n]/.test(file.mimeType) ? { mimeType: file.mimeType } : {}),
17
+ ...(typeof file.expiresAt === "number" && Number.isSafeInteger(file.expiresAt) && file.expiresAt > 0 ? { expiresAt: file.expiresAt } : {}), };
18
+ }
@@ -0,0 +1,11 @@
1
+ import type { Chat } from "./chatContract.js";
2
+ import type { ChatSnapshot, ChatThreadSnapshot } from "./chatTypes.js";
3
+ export interface ChatStore {
4
+ subscribeThread(threadId: string, listener: () => void, fields?: readonly (keyof ChatThreadSnapshot)[]): () => void;
5
+ getSnapshot(): ChatSnapshot;
6
+ getSnapshot(threadId: string): ChatThreadSnapshot;
7
+ subscribeChanges(listener: () => void): () => void;
8
+ subscribeChanges(threadId: string, listener: () => void, fields?: readonly (keyof ChatThreadSnapshot)[]): () => void;
9
+ }
10
+ /** Synchronous memory reads are for external-store integrations, not cache selection. */
11
+ export declare function getChatStore(chat: Chat): ChatStore;
@@ -0,0 +1,12 @@
1
+ import { chatEngine } from "./internal/chatRegistry.js";
2
+ const stores = new WeakMap();
3
+ /** Synchronous memory reads are for external-store integrations, not cache selection. */
4
+ export function getChatStore(chat) {
5
+ let store = stores.get(chat);
6
+ if (!store) {
7
+ const engine = chatEngine(chat);
8
+ store = { subscribeThread: (id, listener, fields) => engine.subscribeThread(id, listener, { fields, emitCurrent: false }), getSnapshot: ((id) => id === undefined ? engine.getSnapshot() : engine.getSnapshot(id)), subscribeChanges: ((target, callback, fields) => typeof target === "function" ? engine.subscribe(target, { emitCurrent: false }) : engine.subscribe(target, callback, { fields, emitCurrent: false })) };
9
+ stores.set(chat, store);
10
+ }
11
+ return store;
12
+ }
@@ -0,0 +1,4 @@
1
+ import type { Chat } from "./chatContract.js";
2
+ import type { ChatThreadSnapshot, ReadOptions } from "./chatTypes.js";
3
+ /** Explicit complete-history operation for search/export; may scan many pages. */
4
+ export declare function loadThreadHistory(chat: Chat, threadId: string, { signal }?: ReadOptions): Promise<ChatThreadSnapshot>;
package/lib/history.js ADDED
@@ -0,0 +1,8 @@
1
+ import { chatEngine } from "./internal/chatRegistry.js";
2
+ import { waitForCaller } from "./credentials.js";
3
+ /** Explicit complete-history operation for search/export; may scan many pages. */
4
+ export function loadThreadHistory(chat, threadId, { signal } = {}) {
5
+ if (signal?.aborted)
6
+ return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
7
+ return waitForCaller(chatEngine(chat).getMessages(threadId, { all: true }), signal);
8
+ }
@@ -0,0 +1,4 @@
1
+ import type { Chat, ChatSnapshot, ChatThreadSnapshot } from "../chat.js";
2
+ /** React consumes the same stable view as callback subscribers. No App-side cache. */
3
+ export declare function useChatSnapshot(chat: Chat | undefined): ChatSnapshot;
4
+ export declare function useChatSnapshot(chat: Chat | undefined, threadId: string): ChatThreadSnapshot;
@@ -0,0 +1,9 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ import { getChatStore } from "../framework.js";
3
+ const emptySummary = Object.freeze({ threads: [], unreadMessageCount: undefined, badgeCount: undefined, connection: "closed" });
4
+ const emptyThread = Object.freeze({ threadId: "", messages: [], status: "pending", ready: false, hasEarlier: false, unread: undefined, group: undefined, readCounts: undefined });
5
+ export function useChatSnapshot(chat, threadId) {
6
+ const subscribe = useCallback((notify) => !chat ? () => { } : threadId === undefined ? getChatStore(chat).subscribeChanges(notify) : getChatStore(chat).subscribeThread(threadId, notify), [chat, threadId]);
7
+ const snapshot = useCallback(() => threadId === undefined ? (chat ? getChatStore(chat).getSnapshot() : undefined) ?? emptySummary : (chat ? getChatStore(chat).getSnapshot(threadId) : undefined) ?? emptyThread, [chat, threadId]);
8
+ return useSyncExternalStore(subscribe, snapshot, () => threadId === undefined ? emptySummary : emptyThread);
9
+ }
@@ -0,0 +1,50 @@
1
+ import type { ChangeEvent, KeyboardEvent, SyntheticEvent } from "react";
2
+ import { type ChatCommand, type ChatCommandConfig } from "../commands";
3
+ import type { MentionConfig } from "../mentions/model";
4
+ /** One input contract for host-owned composers, SDK inputs, @ mentions and / commands. */
5
+ export declare function useComposerSuggestions({ value, onChange, mentions: config, commands, disabled }: {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ mentions?: MentionConfig;
9
+ commands?: ChatCommandConfig;
10
+ disabled?: boolean;
11
+ }): {
12
+ getMentions: () => import("..").ChatMention[];
13
+ menuProps: {
14
+ open: boolean;
15
+ id: string;
16
+ candidates: import("..").MentionCandidate[];
17
+ activeIndex: number;
18
+ onSelect: (candidate: import("..").MentionCandidate) => void;
19
+ onActiveIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
20
+ menuRef: import("react").RefObject<HTMLDivElement | null>;
21
+ label: string;
22
+ emptyText: string;
23
+ };
24
+ commandMenuProps: {
25
+ id: string;
26
+ open: boolean;
27
+ candidates: ChatCommand[];
28
+ activeIndex: number;
29
+ onSelect: (command: ChatCommand) => void;
30
+ onActiveIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
31
+ menuRef: import("react").RefObject<HTMLDivElement | null>;
32
+ label: string;
33
+ emptyText: string;
34
+ };
35
+ inputProps: {
36
+ onChange(event: ChangeEvent<HTMLTextAreaElement>): void;
37
+ onSelect(event: SyntheticEvent<HTMLTextAreaElement>): void;
38
+ onFocus(event: SyntheticEvent<HTMLTextAreaElement>): void;
39
+ onBlur(event: React.FocusEvent<HTMLTextAreaElement>): void;
40
+ onCompositionStart(): void;
41
+ onCompositionEnd(event: SyntheticEvent<HTMLTextAreaElement>): void;
42
+ onKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void;
43
+ role?: "combobox" | undefined;
44
+ "aria-autocomplete"?: "list" | undefined;
45
+ "aria-haspopup"?: "listbox" | undefined;
46
+ "aria-expanded"?: boolean | undefined;
47
+ "aria-controls"?: string | undefined;
48
+ "aria-activedescendant"?: string | undefined;
49
+ };
50
+ };
@@ -0,0 +1,70 @@
1
+ import { useId, useLayoutEffect, useRef, useState } from "react";
2
+ import { useMentions } from "./useMentions";
3
+ import { commandQuery, completeCommand, filterCommands } from "../commands";
4
+ /** One input contract for host-owned composers, SDK inputs, @ mentions and / commands. */
5
+ export function useComposerSuggestions({ value, onChange, mentions: config, commands, disabled = false }) {
6
+ const mentions = useMentions({ value, onChange, config, disabled });
7
+ const id = useId(), inputRef = useRef(null), menuRef = useRef(null);
8
+ const pending = useRef(null);
9
+ const [caret, setCaret] = useState(0), [focused, setFocused] = useState(false), [composing, setComposing] = useState(false);
10
+ const [dismissed, setDismissed] = useState(false), [selected, setSelected] = useState(0), [collapsed, setCollapsed] = useState(true);
11
+ const query = commands?.commands.length && focused && collapsed && !composing && !disabled && !dismissed ? commandQuery(value, caret) : null;
12
+ const candidates = query ? filterCommands(commands.commands, query.query) : [];
13
+ const index = Math.min(selected, Math.max(0, candidates.length - 1));
14
+ useLayoutEffect(() => {
15
+ if (pending.current?.text !== value)
16
+ return;
17
+ const position = pending.current.caret;
18
+ pending.current = null;
19
+ inputRef.current?.focus({ preventScroll: true });
20
+ inputRef.current?.setSelectionRange(position, position);
21
+ setCaret(position);
22
+ }, [value]);
23
+ function observe(event) {
24
+ inputRef.current = event.currentTarget;
25
+ setCaret(event.currentTarget.selectionStart);
26
+ setCollapsed(event.currentTarget.selectionStart === event.currentTarget.selectionEnd);
27
+ }
28
+ function select(command) {
29
+ if (!query)
30
+ return;
31
+ const next = completeCommand(value, query.end, command);
32
+ pending.current = next;
33
+ setDismissed(true);
34
+ onChange(next.text);
35
+ }
36
+ return {
37
+ getMentions: mentions.getMentions,
38
+ menuProps: { ...mentions.menuProps, open: mentions.menuProps.open && !query },
39
+ commandMenuProps: { id, open: Boolean(query), candidates, activeIndex: index, onSelect: select, onActiveIndex: setSelected, menuRef, label: commands?.label ?? "选择命令", emptyText: commands?.emptyText ?? "没有匹配的命令" },
40
+ inputProps: {
41
+ ...mentions.inputProps,
42
+ ...(commands ? { role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": Boolean(query || mentions.menuProps.open), "aria-controls": query ? id : mentions.inputProps["aria-controls"], "aria-activedescendant": query && candidates[index] ? `${id}-${index}` : mentions.inputProps["aria-activedescendant"] } : {}),
43
+ onChange(event) { observe(event); setDismissed(false); setSelected(0); mentions.inputProps.onChange(event); },
44
+ onSelect(event) { observe(event); mentions.inputProps.onSelect(event); },
45
+ onFocus(event) { observe(event); setFocused(true); mentions.inputProps.onFocus(event); },
46
+ onBlur(event) { if (!menuRef.current?.contains(event.relatedTarget))
47
+ setFocused(false); mentions.inputProps.onBlur(event); },
48
+ onCompositionStart() { setComposing(true); mentions.inputProps.onCompositionStart(); },
49
+ onCompositionEnd(event) { setComposing(false); setDismissed(false); observe(event); mentions.inputProps.onCompositionEnd(event); },
50
+ onKeyDown(event) {
51
+ if (query && !event.nativeEvent.isComposing && event.keyCode !== 229) {
52
+ if (event.key === "Escape") {
53
+ event.preventDefault();
54
+ setDismissed(true);
55
+ }
56
+ else if (candidates.length && ["ArrowDown", "ArrowUp"].includes(event.key)) {
57
+ event.preventDefault();
58
+ setSelected((index + (event.key === "ArrowDown" ? 1 : candidates.length - 1)) % candidates.length);
59
+ }
60
+ else if (candidates[index] && ["Enter", "Tab"].includes(event.key) && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
61
+ event.preventDefault();
62
+ select(candidates[index]);
63
+ }
64
+ }
65
+ if (!event.defaultPrevented)
66
+ mentions.inputProps.onKeyDown(event);
67
+ },
68
+ },
69
+ };
70
+ }
package/lib/index.d.ts CHANGED
@@ -41,3 +41,8 @@ export * from "./runtime/socialReadClient";
41
41
  export * from "./ux/imagePaste";
42
42
  export * from "./hooks/useImageAttachments";
43
43
  export * from "./components/ImageAttachmentTray";
44
+ export { createChat, type Chat, ChatError, type ChatThreadSnapshot, type ChatMessage as MessageView, type ChatErrorCode, type ConversationTarget, type OutgoingContent, type CreateChatOptions, type ChatSnapshot, type SentMessage, type ReadOptions, type ReadersQuery, type GroupChanges } from "./chat.js";
45
+ export { useChatSnapshot } from "./hooks/useChatSnapshot.js";
46
+ export * from "./commands";
47
+ export * from "./components/CommandMenu";
48
+ export * from "./hooks/useComposerSuggestions";
package/lib/index.js CHANGED
@@ -41,3 +41,8 @@ export * from "./runtime/socialReadClient";
41
41
  export * from "./ux/imagePaste";
42
42
  export * from "./hooks/useImageAttachments";
43
43
  export * from "./components/ImageAttachmentTray";
44
+ export { createChat, ChatError } from "./chat.js";
45
+ export { useChatSnapshot } from "./hooks/useChatSnapshot.js";
46
+ export * from "./commands";
47
+ export * from "./components/CommandMenu";
48
+ export * from "./hooks/useComposerSuggestions";
@@ -0,0 +1,131 @@
1
+ export type { ChatMessage } from "../chatTypes.js";
2
+ import { type CreateChatOptions } from "../runtime/createChat.js";
3
+ import type { ChatSession } from "../runtime/chatSession.js";
4
+ import type { CachedChatMessage } from "../runtime/cachedMessage.js";
5
+ import type { ConversationTarget } from "../runtime/conversationDirectory.js";
6
+ import type { OutgoingContent as RuntimeContent } from "../runtime/outgoingContent.js";
7
+ export { ChatError } from "../chatError.js";
8
+ export type { ChatErrorCode } from "../chatError.js";
9
+ export type { CreateChatOptions, ConversationTarget };
10
+ export type OutgoingContent = Exclude<RuntimeContent, {
11
+ groupCreate: unknown;
12
+ } | {
13
+ groupUpdate: unknown;
14
+ }>;
15
+ import type { ChatThreadSnapshot, ChatSnapshot, ReadOptions } from "../chatTypes.js";
16
+ export type { ChatGroup, ConversationInfo, ChatThreadSnapshot, ChatSnapshot, ReadOptions } from "../chatTypes.js";
17
+ export type MessageQuery = {
18
+ earlier?: boolean;
19
+ all?: boolean;
20
+ refresh?: boolean;
21
+ };
22
+ export type ReadersQuery = {
23
+ afterProfileId?: string;
24
+ revision?: number;
25
+ };
26
+ export type GroupChanges = {
27
+ expectedRevision?: string;
28
+ addMembers?: readonly string[];
29
+ removeMembers?: readonly string[];
30
+ title?: string;
31
+ avatar?: string;
32
+ };
33
+ export type SendOptions = {
34
+ waitFor?: "accepted" | "delivered";
35
+ };
36
+ export type SentMessage = Readonly<{
37
+ id: string;
38
+ timestamp: number;
39
+ }>;
40
+ type SubscribeOptions<T> = {
41
+ fields?: readonly (keyof T)[];
42
+ emitCurrent?: boolean;
43
+ };
44
+ /** App-facing API. Cache coverage, raw protocol and synchronization stay in advanced. */
45
+ export declare class ChatEngine {
46
+ readonly advanced: ChatSession<CachedChatMessage>;
47
+ private options;
48
+ private groups;
49
+ private group;
50
+ private present;
51
+ private redactedQuotes;
52
+ private messageLists;
53
+ private messages;
54
+ private views;
55
+ private summary?;
56
+ private targets;
57
+ private errors;
58
+ private listeners;
59
+ private closed;
60
+ constructor(advanced: ChatSession<CachedChatMessage>, options: CreateChatOptions);
61
+ private operation;
62
+ private deny;
63
+ private threadOperation;
64
+ start(): Promise<void>;
65
+ /** Resolve a scoped ID locally, registering its target when needed. No remote group is created. */
66
+ resolveThreadId(target: ConversationTarget): Promise<string>;
67
+ /** A trusted recent window. Source selection and synchronization belong to the SDK. */
68
+ getThread(threadId: string, options?: ReadOptions): Promise<ChatThreadSnapshot>;
69
+ loadEarlier(threadId: string, options?: ReadOptions): Promise<ChatThreadSnapshot>;
70
+ refreshThread(threadId: string, options?: ReadOptions): Promise<ChatThreadSnapshot>;
71
+ private readForCaller;
72
+ private threadSubscribers;
73
+ private openQueue;
74
+ private opening;
75
+ private drainOpens;
76
+ /** Listen first, then open the local window; concurrent consumers share the initial read. */
77
+ subscribeThread(threadId: string, listener: (snapshot: ChatThreadSnapshot) => void, options?: SubscribeOptions<ChatThreadSnapshot>): () => void;
78
+ /** @deprecated Use resolveThreadId. Retained only for the migration window. */
79
+ getConversation(target: ConversationTarget): Promise<string>;
80
+ getSnapshot(): ChatSnapshot;
81
+ getSnapshot(threadId: string): ChatThreadSnapshot;
82
+ getMessages(threadId: string, query?: MessageQuery): Promise<ChatThreadSnapshot>;
83
+ sendMessage(threadId: string, content: OutgoingContent, options?: SendOptions): Promise<SentMessage>;
84
+ retryMessage(messageId: string): Promise<SentMessage>;
85
+ subscribe(listener: (snapshot: ChatSnapshot) => void, options?: SubscribeOptions<ChatSnapshot>): () => void;
86
+ subscribe(threadId: string, listener: (snapshot: ChatThreadSnapshot) => void, options?: SubscribeOptions<ChatThreadSnapshot>): () => void;
87
+ private sessionFields;
88
+ private notify;
89
+ observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<void>;
90
+ markRead(threadId: string): Promise<void>;
91
+ getReaders(threadId: string, messageId: string, page?: ReadersQuery, signal?: AbortSignal): Promise<import("../runtime/messageReaders.js").MessageReadersPage>;
92
+ watchReaders(threadId: string | undefined, ids: string[], receiver?: string): Promise<void>;
93
+ handleReach(event: Parameters<ChatSession<CachedChatMessage>["handleReach"]>[0]): Promise<"handled" | undefined>;
94
+ getVoiceAttachment(threadId: string, messageId: string): Promise<import("../runtime/durableSender.js").VoiceAttachmentLease>;
95
+ getGroup(threadId: string, options?: {
96
+ refresh?: boolean;
97
+ }): Promise<Readonly<{
98
+ groupId: string;
99
+ teamId: string;
100
+ ownerId: string;
101
+ title: string;
102
+ avatar?: string;
103
+ memberIds: readonly string[];
104
+ revision: string;
105
+ }>>;
106
+ createGroup(input: {
107
+ groupId?: string;
108
+ title: string;
109
+ members: readonly string[];
110
+ }): Promise<{
111
+ threadId: string;
112
+ groupId: string;
113
+ teamId: string;
114
+ ownerId: string;
115
+ title: string;
116
+ avatar?: string;
117
+ memberIds: readonly string[];
118
+ revision: string;
119
+ }>;
120
+ updateGroup(threadId: string, changes: GroupChanges): Promise<Readonly<{
121
+ groupId: string;
122
+ teamId: string;
123
+ ownerId: string;
124
+ title: string;
125
+ avatar?: string;
126
+ memberIds: readonly string[];
127
+ revision: string;
128
+ }>>;
129
+ revokeAccess(): Promise<void>;
130
+ close(): Promise<void>;
131
+ }