fauna-api 0.0.0-stage → 1.0.679
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/brands.d.ts +2 -0
- package/dist/brands.js +2 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +121 -0
- package/dist/src/ant/chat.d.ts +130 -0
- package/dist/src/ant/chat.js +467 -0
- package/dist/src/ant/format.d.ts +40 -0
- package/dist/src/ant/format.js +143 -0
- package/dist/src/ant/graph.d.ts +130 -0
- package/dist/src/ant/graph.js +449 -0
- package/dist/src/ant/index.d.ts +16 -0
- package/dist/src/ant/index.js +61 -0
- package/dist/src/ant/persona.d.ts +151 -0
- package/dist/src/ant/persona.js +136 -0
- package/dist/src/ant/timeline.d.ts +283 -0
- package/dist/src/ant/timeline.js +1025 -0
- package/dist/src/authentification.d.ts +45 -0
- package/dist/src/authentification.js +194 -0
- package/dist/src/billing.d.ts +135 -0
- package/dist/src/billing.js +128 -0
- package/dist/src/calendar.d.ts +83 -0
- package/dist/src/calendar.js +133 -0
- package/dist/src/chats.d.ts +113 -0
- package/dist/src/chats.js +100 -0
- package/dist/src/cicada/contacts.d.ts +31 -0
- package/dist/src/cicada/contacts.js +28 -0
- package/dist/src/cicada/conversations.d.ts +73 -0
- package/dist/src/cicada/conversations.js +236 -0
- package/dist/src/cicada/inboxes.d.ts +48 -0
- package/dist/src/cicada/inboxes.js +82 -0
- package/dist/src/cicada/index.d.ts +16 -0
- package/dist/src/cicada/index.js +61 -0
- package/dist/src/cicada/phone.d.ts +48 -0
- package/dist/src/cicada/phone.js +306 -0
- package/dist/src/drive.d.ts +80 -0
- package/dist/src/drive.js +472 -0
- package/dist/src/extensions/index.d.ts +4 -0
- package/dist/src/extensions/index.js +5 -0
- package/dist/src/extensions/web/index.d.ts +6 -0
- package/dist/src/extensions/web/index.js +7 -0
- package/dist/src/extensions/web/session.d.ts +108 -0
- package/dist/src/extensions/web/session.js +470 -0
- package/dist/src/extensions/web/tools.d.ts +13 -0
- package/dist/src/extensions/web/tools.js +72 -0
- package/dist/src/friends.d.ts +39 -0
- package/dist/src/friends.js +119 -0
- package/dist/src/groups.d.ts +82 -0
- package/dist/src/groups.js +269 -0
- package/dist/src/inbox.d.ts +11 -0
- package/dist/src/inbox.js +35 -0
- package/dist/src/integrations.d.ts +52 -0
- package/dist/src/integrations.js +205 -0
- package/dist/src/magpie/explorer.d.ts +14 -0
- package/dist/src/magpie/explorer.js +36 -0
- package/dist/src/magpie/index.d.ts +16 -0
- package/dist/src/magpie/index.js +61 -0
- package/dist/src/magpie/portfolio.d.ts +52 -0
- package/dist/src/magpie/portfolio.js +179 -0
- package/dist/src/magpie/quotes.d.ts +35 -0
- package/dist/src/magpie/quotes.js +47 -0
- package/dist/src/magpie/room.d.ts +61 -0
- package/dist/src/magpie/room.js +238 -0
- package/dist/src/maps.d.ts +159 -0
- package/dist/src/maps.js +171 -0
- package/dist/src/marmot/browser.d.ts +139 -0
- package/dist/src/marmot/browser.js +389 -0
- package/dist/src/marmot/index.d.ts +20 -0
- package/dist/src/marmot/index.js +65 -0
- package/dist/src/marmot/media.d.ts +56 -0
- package/dist/src/marmot/media.js +265 -0
- package/dist/src/marmot/research.d.ts +337 -0
- package/dist/src/marmot/research.js +1138 -0
- package/dist/src/marmot/scraper.d.ts +59 -0
- package/dist/src/marmot/scraper.js +94 -0
- package/dist/src/marmot/web.d.ts +402 -0
- package/dist/src/marmot/web.js +2586 -0
- package/dist/src/marmot/workspace.d.ts +81 -0
- package/dist/src/marmot/workspace.js +263 -0
- package/dist/src/parrot/agent.d.ts +53 -0
- package/dist/src/parrot/agent.js +93 -0
- package/dist/src/parrot/chat.d.ts +84 -0
- package/dist/src/parrot/chat.js +237 -0
- package/dist/src/parrot/format.d.ts +128 -0
- package/dist/src/parrot/format.js +639 -0
- package/dist/src/parrot/generative.d.ts +261 -0
- package/dist/src/parrot/generative.js +185 -0
- package/dist/src/parrot/index.d.ts +22 -0
- package/dist/src/parrot/index.js +67 -0
- package/dist/src/parrot/room.d.ts +225 -0
- package/dist/src/parrot/room.js +466 -0
- package/dist/src/parrot/routing.d.ts +90 -0
- package/dist/src/parrot/routing.js +78 -0
- package/dist/src/parrot/share.d.ts +126 -0
- package/dist/src/parrot/share.js +261 -0
- package/dist/src/parrot/socket.d.ts +41 -0
- package/dist/src/parrot/socket.js +478 -0
- package/dist/src/payments.d.ts +73 -0
- package/dist/src/payments.js +107 -0
- package/dist/src/private/cards.css +75 -0
- package/dist/src/rooms.d.ts +76 -0
- package/dist/src/rooms.js +168 -0
- package/dist/src/shared/schema.d.ts +24 -0
- package/dist/src/shared/schema.js +1 -0
- package/dist/src/shared/server.d.ts +65 -0
- package/dist/src/shared/server.js +302 -0
- package/dist/src/termite/environment.d.ts +110 -0
- package/dist/src/termite/environment.js +263 -0
- package/dist/src/termite/index.d.ts +14 -0
- package/dist/src/termite/index.js +59 -0
- package/dist/src/termite/os.d.ts +326 -0
- package/dist/src/termite/os.js +572 -0
- package/dist/src/termite/versioning.d.ts +94 -0
- package/dist/src/termite/versioning.js +217 -0
- package/dist/src/tickets.d.ts +43 -0
- package/dist/src/tickets.js +138 -0
- package/dist/src/travel.d.ts +85 -0
- package/dist/src/travel.js +44 -0
- package/dist/src/ui/authentification.d.ts +24 -0
- package/dist/src/ui/authentification.js +177 -0
- package/dist/src/ui/brands.css +11 -0
- package/dist/src/ui/brands.d.ts +7 -0
- package/dist/src/ui/brands.js +14 -0
- package/dist/src/ui/browser.d.ts +74 -0
- package/dist/src/ui/browser.js +470 -0
- package/dist/src/ui/chat.d.ts +183 -0
- package/dist/src/ui/chat.js +2379 -0
- package/dist/src/ui/chatPanel.d.ts +109 -0
- package/dist/src/ui/chatPanel.js +768 -0
- package/dist/src/ui/chatSequence.d.ts +36 -0
- package/dist/src/ui/chatSequence.js +42 -0
- package/dist/src/ui/crown.d.ts +19 -0
- package/dist/src/ui/crown.js +199 -0
- package/dist/src/ui/cursor.d.ts +73 -0
- package/dist/src/ui/cursor.js +175 -0
- package/dist/src/ui/device.d.ts +33 -0
- package/dist/src/ui/device.js +49 -0
- package/dist/src/ui/drive.d.ts +69 -0
- package/dist/src/ui/drive.js +1243 -0
- package/dist/src/ui/generative.d.ts +39 -0
- package/dist/src/ui/generative.js +313 -0
- package/dist/src/ui/hover.d.ts +6 -0
- package/dist/src/ui/hover.js +210 -0
- package/dist/src/ui/index.d.ts +42 -0
- package/dist/src/ui/index.js +87 -0
- package/dist/src/ui/integrations.d.ts +13 -0
- package/dist/src/ui/integrations.js +175 -0
- package/dist/src/ui/maps.d.ts +87 -0
- package/dist/src/ui/maps.js +547 -0
- package/dist/src/ui/roster.d.ts +32 -0
- package/dist/src/ui/roster.js +163 -0
- package/dist/src/ui/talk.d.ts +12 -0
- package/dist/src/ui/talk.js +560 -0
- package/dist/src/ui/tickets.d.ts +13 -0
- package/dist/src/ui/tickets.js +966 -0
- package/dist/src/ui/toast.d.ts +7 -0
- package/dist/src/ui/toast.js +258 -0
- package/dist/src/ui/tokenText.d.ts +18 -0
- package/dist/src/ui/tokenText.js +96 -0
- package/dist/src/ui/tokenized.d.ts +42 -0
- package/dist/src/ui/tokenized.js +622 -0
- package/dist/src/ui/ui.css +4763 -0
- package/dist/src/ui/view.d.ts +23 -0
- package/dist/src/ui/view.js +584 -0
- package/dist/src/view.d.ts +113 -0
- package/dist/src/view.js +386 -0
- package/dist/src/webhooks.d.ts +66 -0
- package/dist/src/webhooks.js +78 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/package.json +87 -4
- package/README.md +0 -3
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Credentials, ApiKey, Profile } from "../authentification.js";
|
|
2
|
+
import { Function, Role, Tool } from "./generative.js";
|
|
3
|
+
import { Room, Presence, Authorization } from "../shared/server.js";
|
|
4
|
+
import { ChatTarget, AppendResult, Attachment } from "./share.js";
|
|
5
|
+
import type { Repository } from "../termite/versioning.js";
|
|
6
|
+
import type { Group } from "../groups.js";
|
|
7
|
+
export { Attachment } from "./share.js";
|
|
8
|
+
/**
|
|
9
|
+
* One durable message in a room transcript. Parts keep their file refs as
|
|
10
|
+
* streaming `FaunaFile`s (no bytes over the socket) — `download()` them when you
|
|
11
|
+
* need the bytes. `author` is the sender's public Profile (USER turns only).
|
|
12
|
+
*/
|
|
13
|
+
export type RoomMessage = {
|
|
14
|
+
id: string;
|
|
15
|
+
position: number;
|
|
16
|
+
role: Role;
|
|
17
|
+
parts: any[];
|
|
18
|
+
author?: Profile;
|
|
19
|
+
replyTo?: string;
|
|
20
|
+
/** LLM id that produced a MODEL turn (for UI avatar / rehydrate). */
|
|
21
|
+
model?: string;
|
|
22
|
+
created: number;
|
|
23
|
+
/** The name of the API key an agent posted this with, for its author. */
|
|
24
|
+
via?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* One buffered send in the shared room queue. When a member sends while the
|
|
28
|
+
* model is replying, it lands here (visible to all members) instead of the
|
|
29
|
+
* transcript, and drains FIFO into the transcript as replies complete. `id` is
|
|
30
|
+
* the stable per-row key used for unqueue/steer targeting and orders the queue.
|
|
31
|
+
*/
|
|
32
|
+
export type RoomQueueItem = {
|
|
33
|
+
id: string;
|
|
34
|
+
role: Role;
|
|
35
|
+
parts: any[];
|
|
36
|
+
author?: Profile;
|
|
37
|
+
model?: string;
|
|
38
|
+
reasoning?: string;
|
|
39
|
+
replyTo?: string;
|
|
40
|
+
created: number;
|
|
41
|
+
};
|
|
42
|
+
export type RoomToolState = {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
args: Record<string, unknown>;
|
|
46
|
+
status: "running" | "success" | "error";
|
|
47
|
+
result?: string;
|
|
48
|
+
/** What a running tool is waiting on, like the machine starting. */
|
|
49
|
+
progress?: string;
|
|
50
|
+
/** The live room the tool opened, to watch it: a browser, or a machine's desktop, by access code. */
|
|
51
|
+
session?: {
|
|
52
|
+
kind: "browser" | "desktop";
|
|
53
|
+
code: string;
|
|
54
|
+
url?: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export type RoomHooks = Partial<{
|
|
58
|
+
/** A new message landed (any member's send, or the server's model reply). */
|
|
59
|
+
onMessage(room: ChatRoom, message: RoomMessage): void;
|
|
60
|
+
/** The shared queue changed (an item was buffered, released, or unqueued). */
|
|
61
|
+
onQueue(room: ChatRoom): void;
|
|
62
|
+
/** The room's model reply started (true) or finished (false). */
|
|
63
|
+
onThinking(room: ChatRoom, thinking: boolean): void;
|
|
64
|
+
/** The room's model reply is streaming. `text` is the full reply so far;
|
|
65
|
+
* `delta` is the new portion since the last call. Fires as the reply grows.
|
|
66
|
+
* When the turn finishes, the finished reply also arrives via `onMessage` (a
|
|
67
|
+
* durable row). `aborted` is true on the terminal call — the streamed turn
|
|
68
|
+
* finalized or was cancelled; drop the partial and rely on `onMessage`.
|
|
69
|
+
* `files` are what the reply attached so far. */
|
|
70
|
+
onDelta(room: ChatRoom, turn: string, delta: string, text: string, aborted: boolean, tools: RoomToolState[], files: Attachment[]): void;
|
|
71
|
+
/** Originator-only client tool call (evaluate locally, then sendToolResult). */
|
|
72
|
+
onToolCall(room: ChatRoom, call: {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
args: Record<string, unknown>;
|
|
76
|
+
turn?: string;
|
|
77
|
+
}): void;
|
|
78
|
+
/** The chat was renamed. */
|
|
79
|
+
onRenamed(room: ChatRoom, name: string): void;
|
|
80
|
+
/** The chat's repository was attached, swapped or detached. */
|
|
81
|
+
onRepository(room: ChatRoom): void;
|
|
82
|
+
/** The reply pointed everyone to a file (Parrot's `open`): show it. */
|
|
83
|
+
onOpenFile(room: ChatRoom, attachment: Attachment): void;
|
|
84
|
+
/** The chat was deleted/closed by its owner (or the host disconnected). */
|
|
85
|
+
onClosed(room: ChatRoom): void;
|
|
86
|
+
/** The roster changed (someone joined/left or updated presence/status). */
|
|
87
|
+
onPresence(room: ChatRoom): void;
|
|
88
|
+
}>;
|
|
89
|
+
export declare class ChatRoom extends Room {
|
|
90
|
+
private messages;
|
|
91
|
+
private queue;
|
|
92
|
+
private _thinking;
|
|
93
|
+
private clientTools;
|
|
94
|
+
private _name;
|
|
95
|
+
private _systemPrompt;
|
|
96
|
+
private _human;
|
|
97
|
+
private _repository;
|
|
98
|
+
private _owner;
|
|
99
|
+
private _reference;
|
|
100
|
+
private hooks;
|
|
101
|
+
private target;
|
|
102
|
+
private _hasMore;
|
|
103
|
+
private streaming;
|
|
104
|
+
constructor(ws: WebSocket, authorization: Authorization, id: string, target: ChatTarget, hooks: RoomHooks);
|
|
105
|
+
/** The currently-loaded transcript window, in order. */
|
|
106
|
+
getMessages(): RoomMessage[];
|
|
107
|
+
/** The current shared queue (buffered sends), in FIFO order. */
|
|
108
|
+
getQueue(): RoomQueueItem[];
|
|
109
|
+
/** The chat's name. */
|
|
110
|
+
getName(): string;
|
|
111
|
+
/** Room system prompt (not a transcript row). Empty when unset. */
|
|
112
|
+
getSystemPrompt(): string;
|
|
113
|
+
/** True for a people only chat: no model ever replies, so sends never queue. */
|
|
114
|
+
isHuman(): boolean;
|
|
115
|
+
/** The chat's repository, by id. Kept current for everyone in the room. */
|
|
116
|
+
get repository(): number | null;
|
|
117
|
+
/** The saved chat this room is bound to, by id, or null for an unbound room. */
|
|
118
|
+
get chatId(): string | null;
|
|
119
|
+
/** Who owns the chat: `credentials:{id}`, `group:{id}`, `friend:{id}` or `partnership:{id}`. Empty for an unbound room. */
|
|
120
|
+
get owner(): string;
|
|
121
|
+
/** The group that owns the chat, whose repositories it takes; undefined for anyone else's. */
|
|
122
|
+
get group(): Group | undefined;
|
|
123
|
+
/** Attach a repository, or null to detach. Everyone in the room sees the change. */
|
|
124
|
+
setRepository(repository: Repository | null): Promise<void>;
|
|
125
|
+
private readonly repositoryWatchers;
|
|
126
|
+
/** Hear about every repository change, from anyone in the room. Returns a function that stops listening. */
|
|
127
|
+
watchRepository(listener: (repository: number | null) => void): () => void;
|
|
128
|
+
/** Every file in the chat, newest first: what people attached and what Parrot opened. */
|
|
129
|
+
attachments(): Promise<Attachment[]>;
|
|
130
|
+
/** True while the server is streaming the room's model reply. */
|
|
131
|
+
isThinking(): boolean;
|
|
132
|
+
/** True while older transcript pages remain to be loaded (scroll-up). */
|
|
133
|
+
hasMore(): boolean;
|
|
134
|
+
/** The in-progress streamed model reply, or null when none is streaming. */
|
|
135
|
+
streamingReply(): {
|
|
136
|
+
turn: string;
|
|
137
|
+
text: string;
|
|
138
|
+
tools: RoomToolState[];
|
|
139
|
+
files: Attachment[];
|
|
140
|
+
} | null;
|
|
141
|
+
/**
|
|
142
|
+
* Send the reply under way (thinking, and the streamed turn so far) to the hooks
|
|
143
|
+
* again, for a UI that sets its hooks after connecting in the middle of a reply.
|
|
144
|
+
*/
|
|
145
|
+
replayReply(): void;
|
|
146
|
+
/**
|
|
147
|
+
* Load one older page of transcript messages and merge it into the window by
|
|
148
|
+
* position. The cursor is how many messages are already loaded, counted from
|
|
149
|
+
* the newest, so live messages that arrived since connect keep it correct.
|
|
150
|
+
* File parts stream lazily via their `FaunaFile`s.
|
|
151
|
+
*/
|
|
152
|
+
loadOlder(): Promise<RoomMessage[]>;
|
|
153
|
+
/**
|
|
154
|
+
* Append a message to the shared transcript. USER sends are attributed to
|
|
155
|
+
* the caller and nudge the server's debounced model reply. If the room is
|
|
156
|
+
* currently replying the server buffers the send into the shared queue
|
|
157
|
+
* instead (drained FIFO when the reply finishes); the result says which
|
|
158
|
+
* happened. File parts stream their bytes with the message. `model`
|
|
159
|
+
* (optional) is the sender's selected model — the reply uses the last
|
|
160
|
+
* sender's choice.
|
|
161
|
+
*/
|
|
162
|
+
sendMessage(role: Role, parts: any[], options?: {
|
|
163
|
+
replyTo?: string;
|
|
164
|
+
model?: string;
|
|
165
|
+
reasoning?: string;
|
|
166
|
+
/** Function schemas (array = available; singular = force that tool). */
|
|
167
|
+
tools?: Function | Function[];
|
|
168
|
+
}): Promise<AppendResult>;
|
|
169
|
+
/**
|
|
170
|
+
* React to the message at transcript `position`. A reaction is its own USER
|
|
171
|
+
* message (so in a chat with a model it prompts a reply like any send);
|
|
172
|
+
* `on: false` takes it back.
|
|
173
|
+
*/
|
|
174
|
+
react(position: number, emoji: string, on?: boolean): Promise<AppendResult>;
|
|
175
|
+
/** Reply to a client tool_call addressed to this originator. */
|
|
176
|
+
sendToolResult(callId: string, result: string): void;
|
|
177
|
+
/**
|
|
178
|
+
* Register Tool.evaluate handlers for originator-only room tool calls.
|
|
179
|
+
* When a chat-tool-call arrives, matching tools auto-evaluate and reply.
|
|
180
|
+
*/
|
|
181
|
+
setClientTools(tools: Tool[] | Tool | undefined): void;
|
|
182
|
+
/**
|
|
183
|
+
* Remove one of your buffered messages from the shared queue. It never
|
|
184
|
+
* entered the transcript, so this is not an unsend — the server just drops
|
|
185
|
+
* the queue row and it fans out to every member.
|
|
186
|
+
*/
|
|
187
|
+
unqueue(queueId: string): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* Steer the room: stop the in-progress model reply now and have the server
|
|
190
|
+
* re-answer after the normal debounce window. Any member may steer.
|
|
191
|
+
* `queueId` releases just that one queued row into the transcript first
|
|
192
|
+
* (per-item steer); `all` flushes the whole queue in FIFO order (steer-all).
|
|
193
|
+
* Released rows behave like idle sends — the model does NOT reply immediately.
|
|
194
|
+
* `stop` cancels the current reply without scheduling a new one.
|
|
195
|
+
*/
|
|
196
|
+
steer(options?: {
|
|
197
|
+
queueId?: string;
|
|
198
|
+
all?: boolean;
|
|
199
|
+
stop?: boolean;
|
|
200
|
+
model?: string;
|
|
201
|
+
reasoning?: string;
|
|
202
|
+
}): Promise<void>;
|
|
203
|
+
/** Replace/merge the room's event hooks after connection (e.g. once the UI
|
|
204
|
+
* that owns rendering is ready). Later hooks override earlier ones. */
|
|
205
|
+
setHooks(hooks: RoomHooks): void;
|
|
206
|
+
/** Fire onClosed using the latest hooks (setHooks), not the connect-time snapshot. */
|
|
207
|
+
emitClosed(): void;
|
|
208
|
+
/** Broadcast this member's typing state to the room (presence status). */
|
|
209
|
+
setTyping(self: Presence, typing: boolean): void;
|
|
210
|
+
/** Members currently marked as typing (excludes the given self presence). */
|
|
211
|
+
typingPresences(self?: Presence): Presence[];
|
|
212
|
+
hydrate(state: any): void;
|
|
213
|
+
handleEvent(packet: any): boolean;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Connect to a live room as a `ChatRoom`. `target` is an access code or a room
|
|
217
|
+
* reference (`parrot:chat:{chatId}` / `parrot:room:{uuid}`). Resolves with the
|
|
218
|
+
* room and the caller's own `Presence` (use it to broadcast typing + position).
|
|
219
|
+
* The connection snapshot carries only the last transcript window; scroll-up
|
|
220
|
+
* backfill goes through `room.loadOlder()`.
|
|
221
|
+
*/
|
|
222
|
+
export declare function connectRoom(authorization: Credentials | ApiKey, target: ChatTarget, hooks?: RoomHooks): Promise<{
|
|
223
|
+
room: ChatRoom;
|
|
224
|
+
self: Presence;
|
|
225
|
+
}>;
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { Role } from "./generative.js";
|
|
2
|
+
import { Room, Presence, connect } from "../shared/server.js";
|
|
3
|
+
import { Parrot } from "./index.js";
|
|
4
|
+
import { appendRoomMessage, steerRoom, unqueueRoomMessage, pageMessages, setRoomRepository, listRoomAttachments, Attachment, } from "./share.js";
|
|
5
|
+
export { Attachment } from "./share.js";
|
|
6
|
+
/**
|
|
7
|
+
* A live, domain-agnostic chat room. `ChatRoom extends Room` and owns everything
|
|
8
|
+
* chat-transcript-specific: a WINDOWED transcript (the last page hydrated on
|
|
9
|
+
* connect), `loadOlder()` pagination for scroll-up backfill, the shared
|
|
10
|
+
* queue, and the streamed model reply. It is NOT tied to a durable "chat" —
|
|
11
|
+
* Parrot, Extensions.Web, and Marmot's chat side all reuse it. The generic
|
|
12
|
+
* `Room` stays domain-agnostic (presence + deltas) for non-chat rooms.
|
|
13
|
+
*
|
|
14
|
+
* Sends go through the append edge (so file parts stream) and the server owns
|
|
15
|
+
* turn-taking — once the room goes quiet it streams one model reply over the
|
|
16
|
+
* whole transcript, billed to the room's host principal.
|
|
17
|
+
*/
|
|
18
|
+
const OLDER_PAGE = 20;
|
|
19
|
+
export class ChatRoom extends Room {
|
|
20
|
+
constructor(ws, authorization, id, target, hooks) {
|
|
21
|
+
// generateAccessCode → POST `${endpoint}/access-code` (mountRoom base).
|
|
22
|
+
super(`${Parrot.getURL()}/chat/live`, ws, authorization, id);
|
|
23
|
+
Object.defineProperty(this, "messages", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: []
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "queue", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: []
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "_thinking", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: false
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(this, "clientTools", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: []
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(this, "_name", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
value: ""
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(this, "_systemPrompt", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: ""
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(this, "_human", {
|
|
60
|
+
enumerable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
value: false
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(this, "_repository", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
writable: true,
|
|
69
|
+
value: null
|
|
70
|
+
});
|
|
71
|
+
Object.defineProperty(this, "_owner", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
value: ""
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(this, "_reference", {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
value: ""
|
|
82
|
+
});
|
|
83
|
+
Object.defineProperty(this, "hooks", {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
writable: true,
|
|
87
|
+
value: void 0
|
|
88
|
+
});
|
|
89
|
+
Object.defineProperty(this, "target", {
|
|
90
|
+
enumerable: true,
|
|
91
|
+
configurable: true,
|
|
92
|
+
writable: true,
|
|
93
|
+
value: void 0
|
|
94
|
+
});
|
|
95
|
+
Object.defineProperty(this, "_hasMore", {
|
|
96
|
+
enumerable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
writable: true,
|
|
99
|
+
value: false
|
|
100
|
+
});
|
|
101
|
+
// The current streamed model turn so far (by turn id).
|
|
102
|
+
Object.defineProperty(this, "streaming", {
|
|
103
|
+
enumerable: true,
|
|
104
|
+
configurable: true,
|
|
105
|
+
writable: true,
|
|
106
|
+
value: null
|
|
107
|
+
});
|
|
108
|
+
Object.defineProperty(this, "repositoryWatchers", {
|
|
109
|
+
enumerable: true,
|
|
110
|
+
configurable: true,
|
|
111
|
+
writable: true,
|
|
112
|
+
value: new Set()
|
|
113
|
+
});
|
|
114
|
+
this.target = target;
|
|
115
|
+
this.hooks = hooks;
|
|
116
|
+
}
|
|
117
|
+
/** The currently-loaded transcript window, in order. */
|
|
118
|
+
getMessages() {
|
|
119
|
+
return this.messages;
|
|
120
|
+
}
|
|
121
|
+
/** The current shared queue (buffered sends), in FIFO order. */
|
|
122
|
+
getQueue() {
|
|
123
|
+
return this.queue;
|
|
124
|
+
}
|
|
125
|
+
/** The chat's name. */
|
|
126
|
+
getName() {
|
|
127
|
+
return this._name;
|
|
128
|
+
}
|
|
129
|
+
/** Room system prompt (not a transcript row). Empty when unset. */
|
|
130
|
+
getSystemPrompt() {
|
|
131
|
+
return this._systemPrompt;
|
|
132
|
+
}
|
|
133
|
+
/** True for a people only chat: no model ever replies, so sends never queue. */
|
|
134
|
+
isHuman() {
|
|
135
|
+
return this._human;
|
|
136
|
+
}
|
|
137
|
+
/** The chat's repository, by id. Kept current for everyone in the room. */
|
|
138
|
+
get repository() {
|
|
139
|
+
return this._repository;
|
|
140
|
+
}
|
|
141
|
+
/** The saved chat this room is bound to, by id, or null for an unbound room. */
|
|
142
|
+
get chatId() {
|
|
143
|
+
const match = /^parrot:chat:(\d+)$/.exec(this._reference);
|
|
144
|
+
return match ? match[1] : null;
|
|
145
|
+
}
|
|
146
|
+
/** Who owns the chat: `credentials:{id}`, `group:{id}`, `friend:{id}` or `partnership:{id}`. Empty for an unbound room. */
|
|
147
|
+
get owner() {
|
|
148
|
+
return this._owner;
|
|
149
|
+
}
|
|
150
|
+
/** The group that owns the chat, whose repositories it takes; undefined for anyone else's. */
|
|
151
|
+
get group() {
|
|
152
|
+
const match = /^group:(\d+)$/.exec(this._owner);
|
|
153
|
+
return match ? { id: match[1] } : undefined;
|
|
154
|
+
}
|
|
155
|
+
/** Attach a repository, or null to detach. Everyone in the room sees the change. */
|
|
156
|
+
async setRepository(repository) {
|
|
157
|
+
await setRoomRepository(this.getAuthorization(), this.target, repository ? repository.id : null);
|
|
158
|
+
}
|
|
159
|
+
/** Hear about every repository change, from anyone in the room. Returns a function that stops listening. */
|
|
160
|
+
watchRepository(listener) {
|
|
161
|
+
this.repositoryWatchers.add(listener);
|
|
162
|
+
return () => { this.repositoryWatchers.delete(listener); };
|
|
163
|
+
}
|
|
164
|
+
/** Every file in the chat, newest first: what people attached and what Parrot opened. */
|
|
165
|
+
async attachments() {
|
|
166
|
+
return await listRoomAttachments(this.getAuthorization(), this.target);
|
|
167
|
+
}
|
|
168
|
+
/** True while the server is streaming the room's model reply. */
|
|
169
|
+
isThinking() {
|
|
170
|
+
return this._thinking;
|
|
171
|
+
}
|
|
172
|
+
/** True while older transcript pages remain to be loaded (scroll-up). */
|
|
173
|
+
hasMore() {
|
|
174
|
+
return this._hasMore;
|
|
175
|
+
}
|
|
176
|
+
/** The in-progress streamed model reply, or null when none is streaming. */
|
|
177
|
+
streamingReply() {
|
|
178
|
+
return this.streaming;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Send the reply under way (thinking, and the streamed turn so far) to the hooks
|
|
182
|
+
* again, for a UI that sets its hooks after connecting in the middle of a reply.
|
|
183
|
+
*/
|
|
184
|
+
replayReply() {
|
|
185
|
+
if (this._thinking)
|
|
186
|
+
this.hooks.onThinking?.(this, true);
|
|
187
|
+
const s = this.streaming;
|
|
188
|
+
if (s)
|
|
189
|
+
this.hooks.onDelta?.(this, s.turn, s.text, s.text, false, s.tools, s.files);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Load one older page of transcript messages and merge it into the window by
|
|
193
|
+
* position. The cursor is how many messages are already loaded, counted from
|
|
194
|
+
* the newest, so live messages that arrived since connect keep it correct.
|
|
195
|
+
* File parts stream lazily via their `FaunaFile`s.
|
|
196
|
+
*/
|
|
197
|
+
async loadOlder() {
|
|
198
|
+
const page = await pageMessages(this.getAuthorization(), this.target, { cursor: this.messages.length, max: OLDER_PAGE });
|
|
199
|
+
const known = new Set(this.messages.map((m) => m.position));
|
|
200
|
+
const older = page
|
|
201
|
+
.filter((m) => !known.has(m.position))
|
|
202
|
+
.map((m) => ({
|
|
203
|
+
id: m.id,
|
|
204
|
+
position: m.position,
|
|
205
|
+
role: m.role,
|
|
206
|
+
parts: m.parts,
|
|
207
|
+
author: m.author,
|
|
208
|
+
replyTo: m.replyTo,
|
|
209
|
+
model: m.model,
|
|
210
|
+
created: m.created,
|
|
211
|
+
...(m.via ? { via: m.via } : {}),
|
|
212
|
+
}));
|
|
213
|
+
this.messages = [...older, ...this.messages].sort((a, b) => a.position - b.position);
|
|
214
|
+
this._hasMore = page.length === OLDER_PAGE;
|
|
215
|
+
return older;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Append a message to the shared transcript. USER sends are attributed to
|
|
219
|
+
* the caller and nudge the server's debounced model reply. If the room is
|
|
220
|
+
* currently replying the server buffers the send into the shared queue
|
|
221
|
+
* instead (drained FIFO when the reply finishes); the result says which
|
|
222
|
+
* happened. File parts stream their bytes with the message. `model`
|
|
223
|
+
* (optional) is the sender's selected model — the reply uses the last
|
|
224
|
+
* sender's choice.
|
|
225
|
+
*/
|
|
226
|
+
async sendMessage(role, parts, options = {}) {
|
|
227
|
+
return await appendRoomMessage(this.getAuthorization(), this.target, { role, parts, createdAt: new Date() }, options);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* React to the message at transcript `position`. A reaction is its own USER
|
|
231
|
+
* message (so in a chat with a model it prompts a reply like any send);
|
|
232
|
+
* `on: false` takes it back.
|
|
233
|
+
*/
|
|
234
|
+
async react(position, emoji, on = true) {
|
|
235
|
+
const reaction = { type: 'REACTION', emoji, position, on };
|
|
236
|
+
return await this.sendMessage(Role.USER, [reaction]);
|
|
237
|
+
}
|
|
238
|
+
/** Reply to a client tool_call addressed to this originator. */
|
|
239
|
+
sendToolResult(callId, result) {
|
|
240
|
+
this.post({ event: "tool-result", id: callId, result });
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Register Tool.evaluate handlers for originator-only room tool calls.
|
|
244
|
+
* When a chat-tool-call arrives, matching tools auto-evaluate and reply.
|
|
245
|
+
*/
|
|
246
|
+
setClientTools(tools) {
|
|
247
|
+
this.clientTools = !tools ? [] : Array.isArray(tools) ? tools : [tools];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Remove one of your buffered messages from the shared queue. It never
|
|
251
|
+
* entered the transcript, so this is not an unsend — the server just drops
|
|
252
|
+
* the queue row and it fans out to every member.
|
|
253
|
+
*/
|
|
254
|
+
async unqueue(queueId) {
|
|
255
|
+
await unqueueRoomMessage(this.getAuthorization(), this.target, queueId);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Steer the room: stop the in-progress model reply now and have the server
|
|
259
|
+
* re-answer after the normal debounce window. Any member may steer.
|
|
260
|
+
* `queueId` releases just that one queued row into the transcript first
|
|
261
|
+
* (per-item steer); `all` flushes the whole queue in FIFO order (steer-all).
|
|
262
|
+
* Released rows behave like idle sends — the model does NOT reply immediately.
|
|
263
|
+
* `stop` cancels the current reply without scheduling a new one.
|
|
264
|
+
*/
|
|
265
|
+
async steer(options = {}) {
|
|
266
|
+
await steerRoom(this.getAuthorization(), this.target, options);
|
|
267
|
+
}
|
|
268
|
+
/** Replace/merge the room's event hooks after connection (e.g. once the UI
|
|
269
|
+
* that owns rendering is ready). Later hooks override earlier ones. */
|
|
270
|
+
setHooks(hooks) {
|
|
271
|
+
this.hooks = { ...this.hooks, ...hooks };
|
|
272
|
+
}
|
|
273
|
+
/** Fire onClosed using the latest hooks (setHooks), not the connect-time snapshot. */
|
|
274
|
+
emitClosed() {
|
|
275
|
+
// Drop local roster so a remounted widget cannot paint stale peers.
|
|
276
|
+
this.presences.length = 0;
|
|
277
|
+
this.hooks.onClosed?.(this);
|
|
278
|
+
}
|
|
279
|
+
/** Broadcast this member's typing state to the room (presence status). */
|
|
280
|
+
setTyping(self, typing) {
|
|
281
|
+
self.setStatus(typing ? "typing" : "");
|
|
282
|
+
}
|
|
283
|
+
/** Members currently marked as typing (excludes the given self presence). */
|
|
284
|
+
typingPresences(self) {
|
|
285
|
+
return this.presences.filter((p) => p !== self && p.status === "typing");
|
|
286
|
+
}
|
|
287
|
+
// ── internal: hydration + event routing (called by connectRoom) ──────────
|
|
288
|
+
hydrate(state) {
|
|
289
|
+
this._name = state?.chat?.name ?? "";
|
|
290
|
+
this._systemPrompt = typeof state?.chat?.systemPrompt === "string" ? state.chat.systemPrompt : "";
|
|
291
|
+
this._human = state?.chat?.human === true;
|
|
292
|
+
this._repository = typeof state?.chat?.repository === "number" && state.chat.repository > 0 ? state.chat.repository : null;
|
|
293
|
+
this._owner = typeof state?.chat?.owner === "string" ? state.chat.owner : "";
|
|
294
|
+
this._reference = typeof state?.room?.reference === "string" ? state.room.reference : "";
|
|
295
|
+
this.messages = Array.isArray(state?.messages) ? [...state.messages] : [];
|
|
296
|
+
this.queue = Array.isArray(state?.queue) ? [...state.queue] : [];
|
|
297
|
+
this._hasMore = Boolean(state?.hasMore);
|
|
298
|
+
this._thinking = state?.thinking === true;
|
|
299
|
+
const stream = state?.stream;
|
|
300
|
+
this.streaming = stream && typeof stream.turn === "string" && stream.turn
|
|
301
|
+
? {
|
|
302
|
+
turn: stream.turn,
|
|
303
|
+
text: String(stream.text ?? ""),
|
|
304
|
+
tools: Array.isArray(stream.tools) ? stream.tools : [],
|
|
305
|
+
files: (Array.isArray(stream.files) ? stream.files : []).map((raw) => Attachment.fromJSON(raw, this.getAuthorization())),
|
|
306
|
+
}
|
|
307
|
+
: null;
|
|
308
|
+
}
|
|
309
|
+
handleEvent(packet) {
|
|
310
|
+
switch (packet.event) {
|
|
311
|
+
case "chat-message": {
|
|
312
|
+
const message = packet.data?.message;
|
|
313
|
+
if (!message)
|
|
314
|
+
return true;
|
|
315
|
+
if (!this.messages.some((m) => m.id === message.id)) {
|
|
316
|
+
this.messages.push(message);
|
|
317
|
+
this.messages.sort((a, b) => a.position - b.position);
|
|
318
|
+
}
|
|
319
|
+
// A MODEL row finalizes any in-progress streamed turn.
|
|
320
|
+
if (message.role === Role.MODEL)
|
|
321
|
+
this.streaming = null;
|
|
322
|
+
this.hooks.onMessage?.(this, message);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
case "chat-queue-add": {
|
|
326
|
+
const item = packet.data?.item;
|
|
327
|
+
if (!item)
|
|
328
|
+
return true;
|
|
329
|
+
if (!this.queue.some((q) => q.id === item.id)) {
|
|
330
|
+
this.queue.push(item);
|
|
331
|
+
this.queue.sort((a, b) => Number(BigInt(a.id) - BigInt(b.id)));
|
|
332
|
+
}
|
|
333
|
+
this.hooks.onQueue?.(this);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
case "chat-queue-remove": {
|
|
337
|
+
const id = String(packet.data?.id ?? "");
|
|
338
|
+
if (!id)
|
|
339
|
+
return true;
|
|
340
|
+
this.queue = this.queue.filter((q) => q.id !== id);
|
|
341
|
+
this.hooks.onQueue?.(this);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
case "chat-delta": {
|
|
345
|
+
// The server streams the in-progress reply as full text (fans out
|
|
346
|
+
// to every replica). Compute the delta against what we last saw so
|
|
347
|
+
// callers can append incrementally.
|
|
348
|
+
const turn = String(packet.data?.turn ?? "");
|
|
349
|
+
const text = String(packet.data?.text ?? "");
|
|
350
|
+
if (!turn)
|
|
351
|
+
return true;
|
|
352
|
+
if (!this.streaming || this.streaming.turn !== turn) {
|
|
353
|
+
this.streaming = { turn, text: "", tools: [], files: [] };
|
|
354
|
+
}
|
|
355
|
+
const prev = this.streaming.text;
|
|
356
|
+
const delta = text.startsWith(prev) ? text.slice(prev.length) : text;
|
|
357
|
+
const tools = Array.isArray(packet.data?.tools) ? packet.data.tools : [];
|
|
358
|
+
const files = (Array.isArray(packet.data?.files) ? packet.data.files : [])
|
|
359
|
+
.map((raw) => Attachment.fromJSON(raw, this.getAuthorization()));
|
|
360
|
+
this.streaming = { turn, text, tools, files };
|
|
361
|
+
this.hooks.onDelta?.(this, turn, delta, text, false, tools, files);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
case "chat-delta-done": {
|
|
365
|
+
// The stream row was cleared: the turn finalized (a durable
|
|
366
|
+
// chat-message follows) or aborted. Either way, drop the partial —
|
|
367
|
+
// the transcript's MODEL row (if any) is authoritative.
|
|
368
|
+
const turn = String(packet.data?.turn ?? "");
|
|
369
|
+
if (this.streaming && (!turn || this.streaming.turn === turn)) {
|
|
370
|
+
const text = this.streaming.text;
|
|
371
|
+
const doneTurn = this.streaming.turn;
|
|
372
|
+
this.streaming = null;
|
|
373
|
+
this.hooks.onDelta?.(this, doneTurn, "", text, true, [], []);
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
case "chat-status": {
|
|
378
|
+
this._thinking = Boolean(packet.data?.thinking);
|
|
379
|
+
this.hooks.onThinking?.(this, this._thinking);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
case "chat-renamed": {
|
|
383
|
+
this._name = packet.data?.name ?? this._name;
|
|
384
|
+
this.hooks.onRenamed?.(this, this._name);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
case "chat-repository": {
|
|
388
|
+
const name = typeof packet.data?.name === "string" ? packet.data.name : this._name;
|
|
389
|
+
if (name !== this._name) {
|
|
390
|
+
this._name = name;
|
|
391
|
+
this.hooks.onRenamed?.(this, name);
|
|
392
|
+
}
|
|
393
|
+
const raw = packet.data?.repository;
|
|
394
|
+
const repository = typeof raw === "number" && raw > 0 ? raw : null;
|
|
395
|
+
if (repository !== this._repository) {
|
|
396
|
+
this._repository = repository;
|
|
397
|
+
this.hooks.onRepository?.(this);
|
|
398
|
+
for (const listener of this.repositoryWatchers)
|
|
399
|
+
listener(repository);
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
case "chat-open-file": {
|
|
404
|
+
const raw = packet.data?.attachment;
|
|
405
|
+
if (raw)
|
|
406
|
+
this.hooks.onOpenFile?.(this, Attachment.fromJSON(raw, this.getAuthorization()));
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
case "chat-tool-call": {
|
|
410
|
+
const id = String(packet.data?.id ?? packet.id ?? "");
|
|
411
|
+
const name = String(packet.data?.name ?? packet.name ?? "");
|
|
412
|
+
const args = (packet.data?.args ?? packet.args ?? {});
|
|
413
|
+
const turn = packet.data?.turn ? String(packet.data.turn) : undefined;
|
|
414
|
+
if (!id || !name)
|
|
415
|
+
return true;
|
|
416
|
+
const tool = this.clientTools.find((t) => t.function.name === name);
|
|
417
|
+
if (tool) {
|
|
418
|
+
void tool.evaluate(args).then((result) => this.sendToolResult(id, result), (err) => {
|
|
419
|
+
console.error("[chat] tool failed", name, err);
|
|
420
|
+
this.sendToolResult(id, `Error: ${err?.message ?? String(err)}`);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.hooks.onToolCall?.(this, { id, name, args, turn });
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
case "chat-closed":
|
|
429
|
+
case "server-closed": {
|
|
430
|
+
this.emitClosed();
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
default:
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Connect to a live room as a `ChatRoom`. `target` is an access code or a room
|
|
440
|
+
* reference (`parrot:chat:{chatId}` / `parrot:room:{uuid}`). Resolves with the
|
|
441
|
+
* room and the caller's own `Presence` (use it to broadcast typing + position).
|
|
442
|
+
* The connection snapshot carries only the last transcript window; scroll-up
|
|
443
|
+
* backfill goes through `room.loadOlder()`.
|
|
444
|
+
*/
|
|
445
|
+
export async function connectRoom(authorization, target, hooks = {}) {
|
|
446
|
+
const auth = "code" in target
|
|
447
|
+
? { authorization, accessCode: target.code }
|
|
448
|
+
: { authorization, server: target.reference };
|
|
449
|
+
let created;
|
|
450
|
+
const self = await connect(auth, `${Parrot.getURL()}/chat/live`, {
|
|
451
|
+
createRoom: (_endpoint, ws, a, id) => {
|
|
452
|
+
created = new ChatRoom(ws, a, id, target, hooks);
|
|
453
|
+
return created;
|
|
454
|
+
},
|
|
455
|
+
onConnection: (room, state) => {
|
|
456
|
+
room.hydrate(state);
|
|
457
|
+
},
|
|
458
|
+
onEvent: (room, packet) => room.handleEvent(packet),
|
|
459
|
+
// Use ChatRoom.emitClosed so setHooks(onClosed) after UI mount is honored.
|
|
460
|
+
onClosed: (room) => room.emitClosed(),
|
|
461
|
+
createPresence: (room, data) => new Presence(room, data.id, data.profile),
|
|
462
|
+
onPresence: (room) => hooks.onPresence?.(room),
|
|
463
|
+
});
|
|
464
|
+
hooks.onPresence?.(created);
|
|
465
|
+
return { room: created, self };
|
|
466
|
+
}
|