@hanzo/rooms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +23 -0
  4. package/dist/Account.d.ts +8 -0
  5. package/dist/Account.js +58 -0
  6. package/dist/Beluga.d.ts +12 -0
  7. package/dist/Beluga.js +152 -0
  8. package/dist/Billing.d.ts +1 -0
  9. package/dist/Billing.js +31 -0
  10. package/dist/Board.d.ts +9 -0
  11. package/dist/Board.js +447 -0
  12. package/dist/Boxes.d.ts +1 -0
  13. package/dist/Boxes.js +173 -0
  14. package/dist/CalScheduler.d.ts +15 -0
  15. package/dist/CalScheduler.js +196 -0
  16. package/dist/Chat.d.ts +35 -0
  17. package/dist/Chat.js +1049 -0
  18. package/dist/Column.d.ts +15 -0
  19. package/dist/Column.js +88 -0
  20. package/dist/Contacts.d.ts +1 -0
  21. package/dist/Contacts.js +301 -0
  22. package/dist/Directory.d.ts +24 -0
  23. package/dist/Directory.js +356 -0
  24. package/dist/Drive.d.ts +2 -0
  25. package/dist/Drive.js +466 -0
  26. package/dist/Find.d.ts +7 -0
  27. package/dist/Find.js +82 -0
  28. package/dist/Guide.d.ts +6 -0
  29. package/dist/Guide.js +272 -0
  30. package/dist/Home.d.ts +39 -0
  31. package/dist/Home.js +306 -0
  32. package/dist/Inbox.d.ts +9 -0
  33. package/dist/Inbox.js +193 -0
  34. package/dist/Market.d.ts +3 -0
  35. package/dist/Market.js +149 -0
  36. package/dist/Me.d.ts +27 -0
  37. package/dist/Me.js +251 -0
  38. package/dist/Meet.d.ts +9 -0
  39. package/dist/Meet.js +370 -0
  40. package/dist/ModeMark.d.ts +8 -0
  41. package/dist/ModeMark.js +18 -0
  42. package/dist/OrgSwitcher.d.ts +31 -0
  43. package/dist/OrgSwitcher.js +120 -0
  44. package/dist/Orgs.d.ts +10 -0
  45. package/dist/Orgs.js +582 -0
  46. package/dist/Profile.d.ts +4 -0
  47. package/dist/Profile.js +67 -0
  48. package/dist/Prose.d.ts +5 -0
  49. package/dist/Prose.js +111 -0
  50. package/dist/ProviderMark.d.ts +17 -0
  51. package/dist/ProviderMark.js +277 -0
  52. package/dist/Recents.d.ts +10 -0
  53. package/dist/Recents.js +91 -0
  54. package/dist/RightPane.d.ts +13 -0
  55. package/dist/RightPane.js +426 -0
  56. package/dist/Room.d.ts +8 -0
  57. package/dist/Room.js +50 -0
  58. package/dist/Scene.d.ts +12 -0
  59. package/dist/Scene.js +105 -0
  60. package/dist/Settings.d.ts +46 -0
  61. package/dist/Settings.js +321 -0
  62. package/dist/Setup.d.ts +1 -0
  63. package/dist/Setup.js +36 -0
  64. package/dist/Share.d.ts +3 -0
  65. package/dist/Share.js +130 -0
  66. package/dist/Shared.d.ts +1 -0
  67. package/dist/Shared.js +113 -0
  68. package/dist/Shell.d.ts +63 -0
  69. package/dist/Shell.js +1225 -0
  70. package/dist/Start.d.ts +1 -0
  71. package/dist/Start.js +96 -0
  72. package/dist/Support.d.ts +14 -0
  73. package/dist/Support.js +122 -0
  74. package/dist/Talk.d.ts +4 -0
  75. package/dist/Talk.js +107 -0
  76. package/dist/Upgrade.d.ts +11 -0
  77. package/dist/Upgrade.js +75 -0
  78. package/dist/Usage.d.ts +1 -0
  79. package/dist/Usage.js +21 -0
  80. package/dist/Vibe.d.ts +2 -0
  81. package/dist/Vibe.js +188 -0
  82. package/dist/Work.d.ts +12 -0
  83. package/dist/Work.js +256 -0
  84. package/dist/bots/Bots.d.ts +2 -0
  85. package/dist/bots/Bots.js +456 -0
  86. package/dist/bots/Lights.d.ts +8 -0
  87. package/dist/bots/Lights.js +13 -0
  88. package/dist/bots/api.d.ts +120 -0
  89. package/dist/bots/api.js +75 -0
  90. package/dist/bots/face.d.ts +5 -0
  91. package/dist/bots/face.js +13 -0
  92. package/dist/bots/ink.d.ts +7 -0
  93. package/dist/bots/ink.js +29 -0
  94. package/dist/bots/kit.d.ts +113 -0
  95. package/dist/bots/kit.js +43 -0
  96. package/dist/bots/runtime.d.ts +4 -0
  97. package/dist/bots/runtime.js +326 -0
  98. package/dist/card.d.ts +52 -0
  99. package/dist/card.js +52 -0
  100. package/dist/cast.d.ts +143 -0
  101. package/dist/cast.js +251 -0
  102. package/dist/connect.d.ts +29 -0
  103. package/dist/connect.js +64 -0
  104. package/dist/contacts-store.d.ts +111 -0
  105. package/dist/contacts-store.js +276 -0
  106. package/dist/conversations.d.ts +54 -0
  107. package/dist/conversations.js +98 -0
  108. package/dist/copy.d.ts +23 -0
  109. package/dist/copy.js +39 -0
  110. package/dist/crew.d.ts +36 -0
  111. package/dist/crew.js +304 -0
  112. package/dist/dev.d.ts +3 -0
  113. package/dist/dev.js +102 -0
  114. package/dist/enso.d.ts +70 -0
  115. package/dist/enso.js +112 -0
  116. package/dist/failure.d.ts +57 -0
  117. package/dist/failure.js +143 -0
  118. package/dist/ground.d.ts +32 -0
  119. package/dist/ground.js +32 -0
  120. package/dist/host.d.ts +76 -0
  121. package/dist/host.js +76 -0
  122. package/dist/index.d.ts +14 -0
  123. package/dist/index.js +12 -0
  124. package/dist/kept.d.ts +5 -0
  125. package/dist/kept.js +27 -0
  126. package/dist/lib/account.d.ts +49 -0
  127. package/dist/lib/account.js +229 -0
  128. package/dist/lib/ai.d.ts +46 -0
  129. package/dist/lib/ai.js +140 -0
  130. package/dist/lib/api.d.ts +43 -0
  131. package/dist/lib/api.js +62 -0
  132. package/dist/lib/bare.d.ts +9 -0
  133. package/dist/lib/bare.js +7 -0
  134. package/dist/lib/coding.d.ts +105 -0
  135. package/dist/lib/coding.js +87 -0
  136. package/dist/lib/destination.d.ts +102 -0
  137. package/dist/lib/destination.js +260 -0
  138. package/dist/lib/durable.d.ts +66 -0
  139. package/dist/lib/durable.js +107 -0
  140. package/dist/lib/first.d.ts +11 -0
  141. package/dist/lib/first.js +23 -0
  142. package/dist/lib/home.d.ts +61 -0
  143. package/dist/lib/home.js +54 -0
  144. package/dist/lib/host.d.ts +104 -0
  145. package/dist/lib/host.js +132 -0
  146. package/dist/lib/hostname.d.ts +1 -0
  147. package/dist/lib/hostname.js +14 -0
  148. package/dist/lib/hydrated.d.ts +1 -0
  149. package/dist/lib/hydrated.js +20 -0
  150. package/dist/lib/integrations.d.ts +21 -0
  151. package/dist/lib/integrations.js +1 -0
  152. package/dist/lib/intent.d.ts +6 -0
  153. package/dist/lib/intent.js +71 -0
  154. package/dist/lib/invite.d.ts +155 -0
  155. package/dist/lib/invite.js +245 -0
  156. package/dist/lib/limits.d.ts +55 -0
  157. package/dist/lib/limits.js +119 -0
  158. package/dist/lib/list.d.ts +9 -0
  159. package/dist/lib/list.js +11 -0
  160. package/dist/lib/mark.d.ts +13 -0
  161. package/dist/lib/mark.js +85 -0
  162. package/dist/lib/mix.d.ts +22 -0
  163. package/dist/lib/mix.js +22 -0
  164. package/dist/lib/models.d.ts +27 -0
  165. package/dist/lib/models.js +106 -0
  166. package/dist/lib/org.d.ts +109 -0
  167. package/dist/lib/org.js +140 -0
  168. package/dist/lib/path.d.ts +9 -0
  169. package/dist/lib/path.js +29 -0
  170. package/dist/lib/plans.d.ts +193 -0
  171. package/dist/lib/plans.js +310 -0
  172. package/dist/lib/prose.d.ts +13 -0
  173. package/dist/lib/prose.js +62 -0
  174. package/dist/lib/reach.d.ts +17 -0
  175. package/dist/lib/reach.js +70 -0
  176. package/dist/lib/rooms.json +23 -0
  177. package/dist/lib/served.d.ts +20 -0
  178. package/dist/lib/served.js +92 -0
  179. package/dist/lib/session.d.ts +211 -0
  180. package/dist/lib/session.js +493 -0
  181. package/dist/lib/share.d.ts +131 -0
  182. package/dist/lib/share.js +216 -0
  183. package/dist/lib/signout.d.ts +14 -0
  184. package/dist/lib/signout.js +40 -0
  185. package/dist/lib/tags.d.ts +27 -0
  186. package/dist/lib/tags.js +46 -0
  187. package/dist/lib/tap.d.ts +17 -0
  188. package/dist/lib/tap.js +9 -0
  189. package/dist/lib/tier.d.ts +115 -0
  190. package/dist/lib/tier.js +207 -0
  191. package/dist/lib/tiers.d.ts +9 -0
  192. package/dist/lib/tiers.js +141 -0
  193. package/dist/lib/todo.d.ts +125 -0
  194. package/dist/lib/todo.js +379 -0
  195. package/dist/look.d.ts +1 -0
  196. package/dist/look.js +21 -0
  197. package/dist/meters.d.ts +8 -0
  198. package/dist/meters.js +31 -0
  199. package/dist/model.d.ts +4 -0
  200. package/dist/model.js +44 -0
  201. package/dist/network.d.ts +64 -0
  202. package/dist/network.js +216 -0
  203. package/dist/open.d.ts +81 -0
  204. package/dist/open.js +149 -0
  205. package/dist/pane.d.ts +92 -0
  206. package/dist/pane.js +231 -0
  207. package/dist/progress.d.ts +46 -0
  208. package/dist/progress.js +51 -0
  209. package/dist/recent.d.ts +67 -0
  210. package/dist/recent.js +161 -0
  211. package/dist/rooms.d.ts +131 -0
  212. package/dist/rooms.js +195 -0
  213. package/dist/roster.d.ts +28 -0
  214. package/dist/roster.js +70 -0
  215. package/dist/span.d.ts +14 -0
  216. package/dist/span.js +50 -0
  217. package/dist/speech.d.ts +44 -0
  218. package/dist/speech.js +229 -0
  219. package/dist/stars.d.ts +16 -0
  220. package/dist/stars.js +75 -0
  221. package/dist/tabs.d.ts +26 -0
  222. package/dist/tabs.js +25 -0
  223. package/dist/team.d.ts +11 -0
  224. package/dist/team.gen.d.ts +10 -0
  225. package/dist/team.gen.js +11 -0
  226. package/dist/team.js +30 -0
  227. package/dist/tools.d.ts +35 -0
  228. package/dist/tools.js +143 -0
  229. package/dist/tours.d.ts +24 -0
  230. package/dist/tours.js +311 -0
  231. package/dist/waiting.d.ts +15 -0
  232. package/dist/waiting.js +17 -0
  233. package/dist/where.d.ts +54 -0
  234. package/dist/where.js +14 -0
  235. package/package.json +108 -0
package/dist/rooms.js ADDED
@@ -0,0 +1,195 @@
1
+ 'use client';
2
+ // The org's rooms: the one reader and writer for /v1/team.
3
+ //
4
+ // A room is not the sidebar's private business. The rail lists them, /home
5
+ // reads one, and the composer needs the space off it to address anything — so
6
+ // the list, the key, the transcript and the two writes live here, in one shape,
7
+ // and no second module holds another opinion about what a room is.
8
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
9
+ import { useIam } from '@hanzo/iam/react';
10
+ import { useAi } from './lib/ai.js';
11
+ import { where } from './where.js';
12
+ /**
13
+ * The key the workspace selects a room by, and puts in the address:
14
+ * `team:<space>/<id>`. Both halves, because a room id is unique within a space
15
+ * and not across the org. Prefixed rather than hashed, because a key a person
16
+ * can read in a URL is one they can report in a bug.
17
+ */
18
+ export const teamKey = (room) => `team:${room.space}/${room.id}`;
19
+ /** The address a key names, or null for a key that is not a room's. */
20
+ export function roomKey(key) {
21
+ const found = key?.match(/^team:([^/]+)\/(.+)$/);
22
+ return found ? { space: found[1], id: found[2] } : null;
23
+ }
24
+ /** The page a key opens on: Home, carrying it as the `?room=` that
25
+ * `app/(app)/home/_workspace.tsx` reads on arrival. */
26
+ export const roomHref = (key) => `${where().home}?${new URLSearchParams({ room: key })}`;
27
+ /** The prerender's answer, and the first one. A stable reference, because
28
+ * React compares snapshots by identity. */
29
+ const UNKNOWN = { rooms: null, wrong: null };
30
+ let held = UNKNOWN;
31
+ /** Bumped by `reload`; a read is made once per value. */
32
+ let epoch = 0;
33
+ let asked = -1;
34
+ const listeners = new Set();
35
+ function publish(next) {
36
+ held = next;
37
+ for (const one of listeners)
38
+ one();
39
+ }
40
+ function subscribe(one) {
41
+ listeners.add(one);
42
+ return () => {
43
+ listeners.delete(one);
44
+ };
45
+ }
46
+ /** Asks the platform again. The list held stays on screen until it answers. */
47
+ export function reloadRooms() {
48
+ epoch += 1;
49
+ publish({ rooms: held.rooms, wrong: null });
50
+ }
51
+ /**
52
+ * The org's real rooms, from Team, and the way to add one.
53
+ *
54
+ * `/v1/team/rooms` is the org's own list and answers only a validated
55
+ * principal. An archived room is not drawn: it is a room the org closed, and a
56
+ * sidebar that lists them makes leaving one meaningless.
57
+ *
58
+ * NULL IS UNKNOWN AND IT IS NOT EMPTY. Three states, and the caller owes a
59
+ * sentence for each: `null` while nothing is known, `[]` for an org that
60
+ * genuinely has no rooms, and `wrong` for a service that answered no. A list is
61
+ * never fabricated to cover any of them.
62
+ *
63
+ * `open` posts a room and HOLDS the row the platform answered with: a 201 is
64
+ * the stored room, so every view carries it from that moment and the reader can
65
+ * step into it without a second read. The caller is not added to a room by the
66
+ * server, so it names its own members.
67
+ */
68
+ export function useTeamRooms() {
69
+ const { client } = useAi();
70
+ const { user } = useIam();
71
+ const now = useSyncExternalStore(subscribe, () => held, () => UNKNOWN);
72
+ useEffect(() => {
73
+ if (!client || !user || asked === epoch)
74
+ return;
75
+ asked = epoch;
76
+ const mine = epoch;
77
+ client.http
78
+ .json({ path: '/v1/team/rooms' })
79
+ .then((page) => {
80
+ if (mine === epoch)
81
+ publish({ rooms: (page.rooms ?? []).filter((r) => !r.archived), wrong: null });
82
+ })
83
+ .catch((e) => {
84
+ if (mine === epoch)
85
+ publish({ rooms: null, wrong: e });
86
+ });
87
+ }, [client, user, now]);
88
+ const open = useCallback(async (room) => {
89
+ if (!client)
90
+ throw Object.assign(new Error('Sign in to open a room'), { status: 401 });
91
+ const made = await client.http.json({
92
+ method: 'POST',
93
+ path: '/v1/team/rooms',
94
+ body: room,
95
+ });
96
+ publish({ rooms: [...(held.rooms ?? []), made], wrong: null });
97
+ return made;
98
+ }, [client]);
99
+ return { rooms: now.rooms, wrong: now.wrong, reload: reloadRooms, open };
100
+ }
101
+ /** The room with that id, or undefined for none open. */
102
+ export function roomOf(rooms, id) {
103
+ return id && rooms ? rooms.find((r) => r.id === id) : undefined;
104
+ }
105
+ /**
106
+ * One row per id, in the platform's order — `createdOn`, then `id` — so a row
107
+ * held from a send and the same row read back by the next poll sit in one place.
108
+ */
109
+ const order = (rows) => [...new Map(rows.map((one) => [one.id, one])).values()].sort((a, b) => a.createdOn - b.createdOn || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
110
+ /** How often an open room asks what was said. */
111
+ const PACE = 2000;
112
+ /**
113
+ * A room's conversation, and the way to add to it.
114
+ *
115
+ * READ AND WRITE IN ONE HOOK, because they are one question — a pane that can
116
+ * show a room and not answer in it is a directory — and because the poller and
117
+ * the sender then cannot disagree about what the room holds.
118
+ *
119
+ * THE ROOM IS POLLED, every two seconds while it is open and the tab is
120
+ * showing. The team plane has no push a browser can read but its transactor
121
+ * socket, which is a binary protocol rather than a line; a read costs a few
122
+ * hundred milliseconds and nothing on the server past the space's own
123
+ * messages, so half a request a second per open room is the honest price of
124
+ * seeing an answer land. Each read carries its own AbortController, and the
125
+ * cleanup aborts the one in flight and stops the clock — on unmount, and when
126
+ * the room or its space changes.
127
+ *
128
+ * `send` posts and answers the row the platform stored: a 201 is the message,
129
+ * with its id and stamp, so the hook holds it at once and the next poll finds
130
+ * the same id and keeps one. Nothing the platform did not accept is ever held.
131
+ *
132
+ * `space` is half a room's address (two spaces of one org may each hold a room
133
+ * with one id), so both calls carry it and a room with neither reads empty
134
+ * rather than guessing.
135
+ */
136
+ export function useRoomMessages(room, space) {
137
+ const { client } = useAi();
138
+ const { user } = useIam();
139
+ const [messages, setMessages] = useState(null);
140
+ const [failed, setFailed] = useState(null);
141
+ const [read, setRead] = useState(0);
142
+ const again = useCallback(() => setRead((n) => n + 1), []);
143
+ useEffect(() => {
144
+ if (!room) {
145
+ setMessages(null);
146
+ return;
147
+ }
148
+ if (!client || !user || !space)
149
+ return;
150
+ const path = `/v1/team/rooms/${encodeURIComponent(room)}/messages`;
151
+ let stop = null;
152
+ const look = () => {
153
+ stop?.abort();
154
+ stop = new AbortController();
155
+ const { signal } = stop;
156
+ client.http
157
+ .json({ path, query: { space }, signal })
158
+ .then((page) => {
159
+ if (signal.aborted)
160
+ return;
161
+ setFailed(null);
162
+ // An empty room reads empty: `[]` is the answer, not the absence of one.
163
+ setMessages(order(page.messages ?? []));
164
+ })
165
+ .catch((e) => {
166
+ if (!signal.aborted)
167
+ setFailed(e);
168
+ });
169
+ };
170
+ look();
171
+ const clock = setInterval(() => {
172
+ if (!document.hidden)
173
+ look();
174
+ }, PACE);
175
+ return () => {
176
+ clearInterval(clock);
177
+ stop?.abort();
178
+ };
179
+ }, [client, user, room, space, read]);
180
+ /** Says it and answers the stored row, or throws with the platform's status. */
181
+ const send = useCallback(async (text) => {
182
+ if (!client || !room || !space) {
183
+ throw Object.assign(new Error('This room has no address to send to'), { status: 503 });
184
+ }
185
+ const row = await client.http.json({
186
+ method: 'POST',
187
+ path: `/v1/team/rooms/${encodeURIComponent(room)}/messages`,
188
+ body: { space, text },
189
+ });
190
+ setMessages((held) => order([...(held ?? []), row]));
191
+ again();
192
+ return row;
193
+ }, [client, room, space, again]);
194
+ return { messages, failed, send, reload: again };
195
+ }
@@ -0,0 +1,28 @@
1
+ /** One member of the org: a person, or an agent standing in the rooms. */
2
+ export interface Member {
3
+ /** The account a message names as its author. */
4
+ id: string;
5
+ name: string;
6
+ /** A saved picture, as a data URI. People only; an agent wears initials. */
7
+ avatar?: string;
8
+ agent: boolean;
9
+ }
10
+ /**
11
+ * The org's members, or null while unknown.
12
+ *
13
+ * Needs a session and an org: both reads are org-scoped and answer 403 without
14
+ * one. A service account carries no id and can author nothing, so it is not a
15
+ * member here.
16
+ */
17
+ export declare function useRoster(): {
18
+ members: Member[] | null;
19
+ wrong: unknown;
20
+ };
21
+ /** The member an author id names, or undefined for one the roster lacks. */
22
+ export declare const memberOf: (members: Member[] | null, id: string) => Member | undefined;
23
+ /**
24
+ * The reader's own account, from the userinfo the session holds. OIDC names it
25
+ * `sub`, and team's account id is that same uuid — so it is the id a room's
26
+ * members list and a message's author carry for this reader.
27
+ */
28
+ export declare function useMe(): string | null;
package/dist/roster.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+ // Who is in the org, by account — the ONE place an author becomes a name.
3
+ //
4
+ // A message carries the team account that wrote it and nothing else: for a
5
+ // person that is their IAM user id (the OIDC `sub`, which team's account id
6
+ // equals), for an agent the user id the platform minted for it and publishes on
7
+ // `/v1/team/bots`. Two reads, one list, so the transcript, the roster column and
8
+ // the mention chips cannot disagree about what to call anybody.
9
+ import { useEffect, useState } from 'react';
10
+ import { useIam, useOrganizations } from '@hanzo/iam/react';
11
+ import { useAi } from './lib/ai.js';
12
+ /**
13
+ * The org's members, or null while unknown.
14
+ *
15
+ * Needs a session and an org: both reads are org-scoped and answer 403 without
16
+ * one. A service account carries no id and can author nothing, so it is not a
17
+ * member here.
18
+ */
19
+ export function useRoster() {
20
+ const { client } = useAi();
21
+ const { isAuthenticated } = useIam();
22
+ const { currentOrgId } = useOrganizations();
23
+ const [members, setMembers] = useState(null);
24
+ const [wrong, setWrong] = useState(null);
25
+ useEffect(() => {
26
+ if (!client || !isAuthenticated || !currentOrgId)
27
+ return;
28
+ let live = true;
29
+ const stop = new AbortController();
30
+ const { signal } = stop;
31
+ Promise.all([
32
+ client.http.json({ path: '/v1/iam/users', query: { owner: currentOrgId }, signal }),
33
+ client.http.json({
34
+ path: '/v1/team/bots',
35
+ signal,
36
+ }),
37
+ ])
38
+ .then(([people, bots]) => {
39
+ if (!live)
40
+ return;
41
+ setMembers([
42
+ ...(people.users ?? []).flatMap((one) => one.id
43
+ ? [{ id: one.id, name: one.displayName || one.name, avatar: one.avatar || undefined, agent: false }]
44
+ : []),
45
+ ...(bots.bots ?? []).map((one) => ({ id: one.userId, name: one.name, agent: true })),
46
+ ]);
47
+ })
48
+ .catch((e) => {
49
+ if (live)
50
+ setWrong(e);
51
+ });
52
+ return () => {
53
+ live = false;
54
+ stop.abort();
55
+ };
56
+ }, [client, isAuthenticated, currentOrgId]);
57
+ return { members, wrong };
58
+ }
59
+ /** The member an author id names, or undefined for one the roster lacks. */
60
+ export const memberOf = (members, id) => members?.find((one) => one.id === id);
61
+ /**
62
+ * The reader's own account, from the userinfo the session holds. OIDC names it
63
+ * `sub`, and team's account id is that same uuid — so it is the id a room's
64
+ * members list and a message's author carry for this reader.
65
+ */
66
+ export function useMe() {
67
+ const { user } = useIam();
68
+ const sub = user?.sub;
69
+ return typeof sub === 'string' && sub ? sub : null;
70
+ }
package/dist/span.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type CSSProperties } from 'react';
2
+ export interface Span {
3
+ /** The width React knows, px: the kept one, or `initial`. */
4
+ span: number;
5
+ /** The element whose `--span` is the width. */
6
+ ref: (node: unknown) => void;
7
+ /** Spread on that element: it carries `--span`. */
8
+ style: CSSProperties;
9
+ /** The width while a drag moves it, px. */
10
+ move: (n: number) => void;
11
+ /** The width it settled on, px: drawn, known and kept. */
12
+ keep: (n: number) => void;
13
+ }
14
+ export declare function useSpan(store: string, initial: number, floor: number, ceil: number): Span;
package/dist/span.js ADDED
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ // A column's width the reader chose, kept in this browser, and drawn through a
3
+ // CSS custom property so a drag repaints one box instead of re-rendering the
4
+ // column's contents sixty times a second.
5
+ //
6
+ // const col = useSpan('hanzo.app.column', 272, 220, 420)
7
+ // <YStack ref={col.ref} style={col.style}> width: var(--span)
8
+ // <Grip span={col.span} onSpan={col.move} onKeep={col.keep} … /> (@hanzo/build)
9
+ //
10
+ // `move` writes the property on the element and nothing else; `keep` is where
11
+ // React and storage learn the width, once per drag, key or double-click.
12
+ //
13
+ // Storage is read in a layout effect, not during render: the rooms are
14
+ // prerendered, and a render that reads storage draws different HTML on the
15
+ // server and the client. A layout effect runs before the browser paints, so a
16
+ // kept width is on screen from the first frame and the column never jumps.
17
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
18
+ export function useSpan(store, initial, floor, ceil) {
19
+ const [span, setSpan] = useState(initial);
20
+ const el = useRef(null);
21
+ useLayoutEffect(() => {
22
+ try {
23
+ const kept = Number(localStorage.getItem(store));
24
+ if (kept >= floor && kept <= ceil)
25
+ setSpan(kept);
26
+ }
27
+ catch {
28
+ // No store is the default width, which is a state and not a failure.
29
+ }
30
+ }, [store, floor, ceil]);
31
+ // Typed loosely because a gui stack hands its host element through a ref
32
+ // typed for native too; on the web it is the element.
33
+ const ref = useCallback((node) => {
34
+ el.current = node instanceof HTMLElement ? node : null;
35
+ }, []);
36
+ const move = useCallback((n) => {
37
+ el.current?.style.setProperty('--span', `${n}px`);
38
+ }, []);
39
+ const keep = useCallback((n) => {
40
+ el.current?.style.setProperty('--span', `${n}px`);
41
+ setSpan(n);
42
+ try {
43
+ localStorage.setItem(store, String(n));
44
+ }
45
+ catch {
46
+ // A browser that stores nothing still resizes; it just forgets.
47
+ }
48
+ }, [store]);
49
+ return { span, ref, style: { '--span': `${span}px` }, move, keep };
50
+ }
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ /** Strips code blocks, links, headers, and markdown markup for natural audio speech. */
3
+ export declare function cleanForSpeech(raw: string): string;
4
+ export declare function stopAgentSpeech(): void;
5
+ export interface SpeakOptions {
6
+ agent?: string;
7
+ onStart?: () => void;
8
+ onEnd?: () => void;
9
+ onError?: (err: unknown) => void;
10
+ }
11
+ /**
12
+ * Speak text in the assigned voice of an agent.
13
+ * Returns a cancel function.
14
+ */
15
+ export declare function speakAgent(text: string, options?: SpeakOptions): () => void;
16
+ /**
17
+ * Hook for managing agent speech playback in UI components.
18
+ */
19
+ export declare function useAgentSpeech(defaultAgent?: string): {
20
+ speaking: boolean;
21
+ currentText: string;
22
+ speak: (text: string, agentName?: string) => void;
23
+ stop: () => void;
24
+ toggle: (text: string, agentName?: string) => void;
25
+ };
26
+ /** A colour the wave or the button paints with: a design token's custom property. */
27
+ type Ink = `var(--${string})`;
28
+ /**
29
+ * Animated Sound Wave visualizer shown when an agent is speaking.
30
+ */
31
+ export declare function AudioWave({ active, color }: {
32
+ active?: boolean;
33
+ color?: Ink;
34
+ }): React.JSX.Element;
35
+ /**
36
+ * SpeakButton: Placed on assistant messages in Chat and Talk channels.
37
+ * Allows one-click audio playback in the agent's voice.
38
+ */
39
+ export declare function SpeakButton({ text, agentName, compact, }: {
40
+ text: string;
41
+ agentName?: string;
42
+ compact?: boolean;
43
+ }): React.JSX.Element;
44
+ export {};
package/dist/speech.js ADDED
@@ -0,0 +1,229 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // Agent Speech & Voice Synthesis
4
+ //
5
+ // Every agent in the Hanzo ecosystem has an assigned voice (`voiceOf(name)` from
6
+ // `cast.tsx`). This module provides speech synthesis for agents across Chat,
7
+ // Talk channels, and Meet video calls:
8
+ // - Direct browser SpeechSynthesis with agent-tailored pitch, rate, and voice matching
9
+ // - Audio wave visualization during playback
10
+ // - Clean cancellation and state management
11
+ // - Text normalization (strips code fences, markdown syntax so speech sounds natural)
12
+ import { useEffect, useRef, useState, useCallback } from 'react';
13
+ import { Volume2, Square } from 'lucide-react';
14
+ import { View, XStack } from '@hanzo/gui';
15
+ import { Button } from '@hanzo/ui';
16
+ import { GOOD } from './lib/mix.js';
17
+ import { voiceOf, voiceProfileOf } from './cast.js';
18
+ /** Strips code blocks, links, headers, and markdown markup for natural audio speech. */
19
+ export function cleanForSpeech(raw) {
20
+ if (!raw)
21
+ return '';
22
+ return raw
23
+ // Replace fenced code blocks with brief spoken indicator
24
+ .replace(/```[\s\S]*?```/g, ' [code snippet] ')
25
+ // Replace inline code `code` with code
26
+ .replace(/`([^`]+)`/g, '$1')
27
+ // Replace markdown links [text](url) with text
28
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
29
+ // Remove markdown headers #, ##, ###
30
+ .replace(/^#{1,6}\s+/gm, '')
31
+ // Remove bold/italic markers
32
+ .replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1')
33
+ // Remove blockquotes
34
+ .replace(/^>\s+/gm, '')
35
+ // Remove horizontal rules
36
+ .replace(/^[-*_]{3,}\s*$/gm, '')
37
+ // Remove bullet points
38
+ .replace(/^[\s]*[-+*]\s+/gm, '')
39
+ // Replace multiple newlines or spaces
40
+ .replace(/\n+/g, '. ')
41
+ .replace(/\s+/g, ' ')
42
+ .trim();
43
+ }
44
+ /** Currently active speech synthesis utterance tracker. */
45
+ let activeUtterance = null;
46
+ const activeListeners = new Set();
47
+ export function stopAgentSpeech() {
48
+ if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
49
+ try {
50
+ window.speechSynthesis.cancel();
51
+ }
52
+ catch { }
53
+ }
54
+ activeUtterance = null;
55
+ for (const notify of activeListeners) {
56
+ notify();
57
+ }
58
+ }
59
+ /**
60
+ * Speak text in the assigned voice of an agent.
61
+ * Returns a cancel function.
62
+ */
63
+ export function speakAgent(text, options = {}) {
64
+ if (typeof window === 'undefined')
65
+ return () => { };
66
+ stopAgentSpeech();
67
+ const clean = cleanForSpeech(text);
68
+ if (!clean)
69
+ return () => { };
70
+ if (!('speechSynthesis' in window)) {
71
+ options.onError?.(new Error('SpeechSynthesis not supported in this browser'));
72
+ return () => { };
73
+ }
74
+ const profile = voiceProfileOf(options.agent);
75
+ const utterance = new SpeechSynthesisUtterance(clean);
76
+ utterance.pitch = profile.pitch;
77
+ utterance.rate = profile.rate;
78
+ // Attempt to select best matching system voice based on gender / voice tone
79
+ try {
80
+ const voices = window.speechSynthesis.getVoices();
81
+ if (voices.length > 0) {
82
+ const isFemale = profile.gender === 'female';
83
+ const candidate = voices.find((v) => {
84
+ const name = v.name.toLowerCase();
85
+ const lang = v.lang.toLowerCase();
86
+ if (!lang.startsWith('en'))
87
+ return false;
88
+ if (isFemale) {
89
+ return name.includes('female') || name.includes('samantha') || name.includes('victoria') || name.includes('karen') || name.includes('zira');
90
+ }
91
+ else {
92
+ return name.includes('male') || name.includes('daniel') || name.includes('alex') || name.includes('fred') || name.includes('david');
93
+ }
94
+ }) || voices.find((v) => v.lang.toLowerCase().startsWith('en')) || voices[0];
95
+ if (candidate) {
96
+ utterance.voice = candidate;
97
+ }
98
+ }
99
+ }
100
+ catch { }
101
+ utterance.onstart = () => {
102
+ activeUtterance = utterance;
103
+ options.onStart?.();
104
+ };
105
+ utterance.onend = () => {
106
+ if (activeUtterance === utterance) {
107
+ activeUtterance = null;
108
+ }
109
+ options.onEnd?.();
110
+ for (const notify of activeListeners) {
111
+ notify();
112
+ }
113
+ };
114
+ utterance.onerror = (e) => {
115
+ if (activeUtterance === utterance) {
116
+ activeUtterance = null;
117
+ }
118
+ options.onError?.(e);
119
+ for (const notify of activeListeners) {
120
+ notify();
121
+ }
122
+ };
123
+ try {
124
+ window.speechSynthesis.speak(utterance);
125
+ }
126
+ catch (err) {
127
+ options.onError?.(err);
128
+ }
129
+ return () => {
130
+ if (activeUtterance === utterance) {
131
+ stopAgentSpeech();
132
+ }
133
+ };
134
+ }
135
+ /**
136
+ * Hook for managing agent speech playback in UI components.
137
+ */
138
+ export function useAgentSpeech(defaultAgent) {
139
+ const [speaking, setSpeaking] = useState(false);
140
+ const [currentText, setCurrentText] = useState(null);
141
+ const cancelRef = useRef(null);
142
+ useEffect(() => {
143
+ const listener = () => {
144
+ setSpeaking(false);
145
+ setCurrentText(null);
146
+ };
147
+ activeListeners.add(listener);
148
+ return () => {
149
+ activeListeners.delete(listener);
150
+ if (cancelRef.current)
151
+ cancelRef.current();
152
+ };
153
+ }, []);
154
+ const stop = useCallback(() => {
155
+ stopAgentSpeech();
156
+ setSpeaking(false);
157
+ setCurrentText(null);
158
+ }, []);
159
+ const speak = useCallback((text, agentName) => {
160
+ stop();
161
+ setCurrentText(text);
162
+ setSpeaking(true);
163
+ cancelRef.current = speakAgent(text, {
164
+ agent: agentName || defaultAgent,
165
+ onStart: () => setSpeaking(true),
166
+ onEnd: () => {
167
+ setSpeaking(false);
168
+ setCurrentText(null);
169
+ },
170
+ onError: () => {
171
+ setSpeaking(false);
172
+ setCurrentText(null);
173
+ },
174
+ });
175
+ }, [defaultAgent, stop]);
176
+ const toggle = useCallback((text, agentName) => {
177
+ if (speaking && currentText === text) {
178
+ stop();
179
+ }
180
+ else {
181
+ speak(text, agentName);
182
+ }
183
+ }, [speaking, currentText, speak, stop]);
184
+ return { speaking, currentText, speak, stop, toggle };
185
+ }
186
+ /** The five bars' resting heights, as a share of the 12px the wave stands in. */
187
+ const BARS = [0.4, 0.9, 0.6, 1, 0.5];
188
+ /**
189
+ * One bar of the wave. It rises and falls between 3px and 13px through the Web
190
+ * Animations API, the same keyframes a stylesheet would hold, so no sheet is
191
+ * written into the page for it; each bar starts 150ms after the one before.
192
+ * A reader who asked for less motion gets the bars standing still.
193
+ */
194
+ function Bar({ share, i, active, color }) {
195
+ const ref = useRef(null);
196
+ useEffect(() => {
197
+ const el = ref.current;
198
+ if (!active || !el?.animate || matchMedia('(prefers-reduced-motion: reduce)').matches)
199
+ return;
200
+ const run = el.animate([{ height: '3px' }, { height: '13px' }], {
201
+ duration: 800,
202
+ delay: i * 150,
203
+ easing: 'ease-in-out',
204
+ iterations: Infinity,
205
+ direction: 'alternate',
206
+ });
207
+ return () => run.cancel();
208
+ }, [active, i]);
209
+ return (_jsx(View, { ref: ref, render: "span", width: 2, height: active ? Math.round(share * 12) : 4, bg: color, rounded: 1 }));
210
+ }
211
+ /**
212
+ * Animated Sound Wave visualizer shown when an agent is speaking.
213
+ */
214
+ export function AudioWave({ active = true, color = 'var(--state-success)' }) {
215
+ return (_jsx(XStack, { render: "span", "aria-hidden": true, display: "inline-flex", items: "center", gap: 2, height: 14, children: BARS.map((share, i) => (_jsx(Bar, { share: share, i: i, active: active, color: color }, i))) }));
216
+ }
217
+ /**
218
+ * SpeakButton: Placed on assistant messages in Chat and Talk channels.
219
+ * Allows one-click audio playback in the agent's voice.
220
+ */
221
+ export function SpeakButton({ text, agentName, compact = false, }) {
222
+ const { speaking, toggle } = useAgentSpeech(agentName);
223
+ const voice = voiceOf(agentName);
224
+ // An @hanzo/ui Button: ghost at rest, the quiet pressed ground while it
225
+ // speaks. The word for speaking is the one status here, so it alone takes
226
+ // the success ink (GOOD holds 4.5:1 on either theme); the icons follow the
227
+ // label through currentColor.
228
+ return (_jsx(Button, { variant: speaking ? 'secondary' : 'ghost', size: compact ? 'icon-sm' : 'sm', color: speaking ? GOOD : '$soft', onClick: () => toggle(text, agentName), "aria-label": speaking ? 'Stop speaking' : `Listen to ${agentName || 'agent'} (${voice})`, "aria-pressed": speaking, title: speaking ? 'Stop speaking' : `Listen in ${agentName || 'agent'}'s voice (${voice})`, children: speaking ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: GOOD }), !compact && 'Speaking…', _jsx(Square, { size: 10, fill: "currentColor" })] })) : (_jsxs(_Fragment, { children: [_jsx(Volume2, { size: 13 }), !compact && 'Listen'] })) }));
229
+ }
@@ -0,0 +1,16 @@
1
+ /** How a room is written wherever a person reads it — a sigil and the name.
2
+ * One formula, because the header and the sidebar star the same room and a
3
+ * second spelling of its title would star two different things. */
4
+ export declare const titleOf: (name: string, direct?: boolean) => string;
5
+ /**
6
+ * The starred rooms, and the toggle that sets them.
7
+ *
8
+ * Read in an effect rather than in the initialiser, because storage is a fact
9
+ * only the browser holds and reading it during the render that hydrates is how
10
+ * the server and the client disagree about a filled star.
11
+ *
12
+ * Every hook instance listens for the change, so starring in the header lights
13
+ * the sidebar in the same tick: two components, one set, no prop threaded
14
+ * between them through the four components that separate them.
15
+ */
16
+ export declare function useStarred(kind?: string): [Set<string>, (id: string) => void];