@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
@@ -0,0 +1,13 @@
1
+ import type { ChatMessage } from '@hanzo/ai/react';
2
+ /**
3
+ * The column. `said` is the conversation it stands beside; without one it draws
4
+ * the same shape with nothing in it, which is what a room nobody has spoken in
5
+ * honestly looks like.
6
+ */
7
+ export declare function RightPane({ said, onClose, }: {
8
+ said?: ChatMessage[];
9
+ onClose?: () => void;
10
+ }): import("react").JSX.Element;
11
+ /** Shows or hides the summary above this conversation's browser. Exported for
12
+ * the conversation header, which is where the control belongs. */
13
+ export declare function usePinned(): [boolean, () => void];
@@ -0,0 +1,426 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The column beside a conversation: what it produced, what went into it, and a
4
+ // browser for looking at any of it.
5
+ //
6
+ // WHAT THIS REPLACED. It was a channel tree — `remington`, `selectric`,
7
+ // `zed-student-ambassador`, `londonmaxxing` — none of which are this product's
8
+ // channels, hand-written into the markup beside a roster of eleven agents. It
9
+ // drew a NAVIGATION surface in the one place the frame reserves for a room's
10
+ // own contents, duplicating the rail on the opposite edge, and it did it in
11
+ // sky blue against a design that has no hue in it. Nothing here reads a list
12
+ // somebody typed.
13
+ //
14
+ // OUTPUTS AND SOURCES ARE DERIVED FROM THE TURNS, never from a second list kept
15
+ // in step with them. `LLM.md` already states this rule for `Work` and it is the
16
+ // same rule: the conversation is the record, so a fenced block in an answer IS
17
+ // an output and an image in a question IS a source. There is nothing to sync,
18
+ // nothing to invalidate, and no count that can disagree with the transcript —
19
+ // and switching channels costs no request, because the turns are already in
20
+ // hand. A conversation that has produced nothing says so.
21
+ //
22
+ // THE COLUMN IS STACKED, NOT SPLIT. The reference this follows puts a ~300px
23
+ // summary beside a wide browser, which needs about 900px of column; the frame
24
+ // gives this one 200–480 (`FLOOR`/`CEIL` in `Shell.tsx`), so side by side would
25
+ // leave the browser about 80px wide. Stacked, both are usable at every width
26
+ // the frame allows, and the pin earns its keep — hiding the summary gives the
27
+ // browser the whole height rather than a slightly wider strip.
28
+ //
29
+ // The frame owns the column; this is one of the things a room puts in it. It
30
+ // draws no column of its own — see `Beside` in `Shell.tsx`.
31
+ import { useMemo, useState } from 'react';
32
+ import { ExternalLink, FileCode, Image as ImageMark, Paperclip, PanelRightClose, Plus, RotateCw, X, } from 'lucide-react';
33
+ import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Text, View, XStack, YStack, } from '@hanzo/ui';
34
+ import { Card, Quiet, Row } from './card.js';
35
+ import { useOpen } from './open.js';
36
+ import { address, channel, hold, name as titleOf, openTab, pickTab, pin, shutTab, usePane, } from './pane.js';
37
+ import { useProviders, useSkills } from './tools.js';
38
+ import { activate, startDevice } from './connect.js';
39
+ /** How many rows a card shows before it offers the rest. */
40
+ const FEW = 5;
41
+ /** Fenced blocks, as a model writes them. The info string is whatever followed
42
+ * the ticks — a language, or a path when the answer named one. */
43
+ const FENCE = /```([^\n`]*)\n([\s\S]*?)```/g;
44
+ /** What a fence's info string means for the browser that will render it. */
45
+ const MIME = {
46
+ html: 'text/html',
47
+ htm: 'text/html',
48
+ svg: 'image/svg+xml',
49
+ css: 'text/css',
50
+ csv: 'text/csv',
51
+ json: 'application/json',
52
+ md: 'text/markdown',
53
+ markdown: 'text/markdown',
54
+ txt: 'text/plain',
55
+ };
56
+ /** The extension, lowercased, or '' where a name has none. */
57
+ const suffix = (s) => {
58
+ const dot = s.lastIndexOf('.');
59
+ return dot > 0 ? s.slice(dot + 1).toLowerCase() : '';
60
+ };
61
+ /**
62
+ * What a fence is called.
63
+ *
64
+ * AN INFO STRING IS A FILENAME OR A LANGUAGE, and telling them apart is the
65
+ * whole of it: a model writing to a file names it, and one showing a snippet
66
+ * names the language. A dot and no spaces is a name; anything else is a
67
+ * language, said as what it is rather than dressed up as a file that does not
68
+ * exist.
69
+ */
70
+ const fenceName = (info, n) => {
71
+ const word = info.trim().split(/\s+/)[0] ?? '';
72
+ if (!word)
73
+ return `Block ${n}`;
74
+ if (word.includes('.') && suffix(word))
75
+ return word;
76
+ return `${word} block`;
77
+ };
78
+ /** The text of a turn, whichever shape the wire used. */
79
+ const words = (m) => typeof m.content === 'string'
80
+ ? m.content
81
+ : Array.isArray(m.content)
82
+ ? m.content.map((p) => (p.type === 'text' ? p.text : '')).join('\n')
83
+ : '';
84
+ /** Every image a turn carries, as addresses. */
85
+ const pictures = (m) => Array.isArray(m.content)
86
+ ? m.content.flatMap((p) => (p.type === 'image_url' && p.image_url?.url ? [p.image_url.url] : []))
87
+ : [];
88
+ /**
89
+ * What the assistant made in this conversation.
90
+ *
91
+ * Fenced blocks and pictures, in the order they were said. The id carries the
92
+ * turn's index so it survives a re-render without a counter of its own, and so
93
+ * two identical blocks in one conversation stay two rows.
94
+ */
95
+ function outputs(said) {
96
+ const out = [];
97
+ said.forEach((m, turn) => {
98
+ if (m.role !== 'assistant')
99
+ return;
100
+ let n = 0;
101
+ for (const [, info, body] of words(m).matchAll(FENCE)) {
102
+ n += 1;
103
+ const named = fenceName(info, n);
104
+ out.push({
105
+ id: `o${turn}.${n}`,
106
+ name: named,
107
+ kind: 'code',
108
+ body,
109
+ mime: MIME[suffix(named) || info.trim().toLowerCase()] ?? 'text/plain',
110
+ });
111
+ }
112
+ pictures(m).forEach((href, i) => {
113
+ out.push({ id: `oi${turn}.${i}`, name: 'Generated image', kind: 'image', href });
114
+ });
115
+ });
116
+ return out;
117
+ }
118
+ /** What went into this conversation: pictures asked about, and files brought in. */
119
+ function sources(said, held) {
120
+ const out = [];
121
+ said.forEach((m, turn) => {
122
+ if (m.role !== 'user')
123
+ return;
124
+ pictures(m).forEach((href, i) => {
125
+ out.push({ id: `s${turn}.${i}`, name: 'Attached image', kind: 'image', href });
126
+ });
127
+ });
128
+ for (const h of held) {
129
+ out.push({
130
+ id: h.id,
131
+ name: h.name,
132
+ kind: h.type.startsWith('image/') ? 'image' : 'file',
133
+ href: h.href,
134
+ mime: h.type,
135
+ });
136
+ }
137
+ return out;
138
+ }
139
+ const GLYPH = {
140
+ image: ImageMark,
141
+ code: FileCode,
142
+ file: Paperclip,
143
+ };
144
+ /**
145
+ * The column. `said` is the conversation it stands beside; without one it draws
146
+ * the same shape with nothing in it, which is what a room nobody has spoken in
147
+ * honestly looks like.
148
+ */
149
+ export function RightPane({ said = [], onClose, }) {
150
+ const open = useOpen();
151
+ // ONE KEY, DERIVED, so the pane follows the conversation without anything
152
+ // having to tell it that the conversation changed.
153
+ const at = channel(open);
154
+ const pane = usePane(at);
155
+ return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", borderLeftWidth: 1, borderColor: "$borderColor", children: [pane.pinned ? _jsx(Summary, { at: at, said: said, held: pane.held }) : null, _jsx(Browser, { at: at, pane: pane, onClose: onClose })] }));
156
+ }
157
+ /** The two cards: out of the conversation, and into it. */
158
+ function Summary({ at, said, held }) {
159
+ // Recomputed only when the turns or the held files change — a channel switch
160
+ // remounts with different turns and costs one pass over them, never a read.
161
+ const made = useMemo(() => outputs(said), [said]);
162
+ const took = useMemo(() => sources(said, held), [said, held]);
163
+ return (_jsxs(YStack, { shrink: 0,
164
+ // A CAP AND NOT A HEIGHT. The summary is as tall as it needs to be until
165
+ // it would take the browser's half, and scrolls after that — so two rows
166
+ // do not reserve a screen and thirty do not push the browser off the
167
+ // bottom of the column.
168
+ maxH: "45%", overflow: "scroll", p: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Marks, { at: at, kind: "outputs", title: "Outputs", icon: _jsx(FileCode, { size: 16, "aria-hidden": true }), marks: made, children: "This conversation has produced nothing yet." }), _jsx(Marks, { at: at, kind: "sources", title: "Sources", icon: _jsx(Paperclip, { size: 16, "aria-hidden": true }), marks: took, children: "Nothing has been put into this conversation yet." })] }));
169
+ }
170
+ /**
171
+ * One card of things, the first {@link FEW} of them, and the honest number of
172
+ * the rest.
173
+ *
174
+ * THE COUNT IS THE LIST'S OWN LENGTH. A row reading "Show 30 more" that is not
175
+ * thirty is the kind of furniture this repo has had to delete twice.
176
+ */
177
+ function Marks({ at, kind, title, icon, marks, children, }) {
178
+ const [all, setAll] = useState(false);
179
+ const shown = all ? marks : marks.slice(0, FEW);
180
+ const rest = marks.length - shown.length;
181
+ return (_jsxs(Card, { kind: kind, icon: icon, title: title, action: _jsx(Add, { at: at }), children: [marks.length === 0 ? _jsx(Quiet, { children: children }) : null, shown.map((m) => {
182
+ const Mark = GLYPH[m.kind];
183
+ return (_jsx(Row, { icon: _jsx(Mark, { size: 16, "aria-hidden": true }), onPress: () => look(at, m), children: m.name }, m.id));
184
+ }), rest > 0 ? (_jsxs(Row, { onPress: () => setAll(true), children: ["Show ", rest, " more"] })) : all && marks.length > FEW ? (_jsx(Row, { onPress: () => setAll(false), children: "Show fewer" })) : null] }));
185
+ }
186
+ /**
187
+ * Opens a mark in this conversation's browser.
188
+ *
189
+ * A BLOCK BECOMES A FILE THE MOMENT SOMEBODY LOOKS AT IT. It has no address
190
+ * until then — it is text inside a turn — so one is minted here from its own
191
+ * bytes, carrying the type its fence declared. That is what makes an answer's
192
+ * `index.html` something you can actually see rather than something you can
193
+ * only read the source of.
194
+ */
195
+ function look(at, m) {
196
+ const href = m.href ?? (m.body === undefined ? null : URL.createObjectURL(new Blob([m.body], { type: m.mime })));
197
+ if (!href)
198
+ return;
199
+ openTab(at, href, m.name);
200
+ }
201
+ /**
202
+ * A menu row's words.
203
+ *
204
+ * gui RENDERS NO BARE STRING. Every view is a react-native-web `View`, which
205
+ * refuses a raw text child — `DropdownMenuItem` happens to wrap one for you and
206
+ * `DropdownMenuSubTrigger` does not, so a label written the obvious way threw
207
+ * "Unexpected text node" and took the whole menu down with it. One wrapper for
208
+ * every row means the difference between those two components stops mattering.
209
+ */
210
+ const Word = ({ children }) => (_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: children }));
211
+ /** A menu row that is a statement rather than a control — what is being read,
212
+ * what was refused, which code to type. Wrapped for the same reason. */
213
+ const Note = ({ children }) => (_jsx(DropdownMenuLabel, { children: _jsx(Text, { fontSize: "$2", color: "$soft", children: children }) }));
214
+ /**
215
+ * The `+`: everything a reader can bring into this conversation.
216
+ *
217
+ * EVERY ROW DOES SOMETHING. Files come off the reader's own disk. Skills are
218
+ * `GET /v1/tools/skills` and toggling one is `PUT /v1/tools/activation`, which
219
+ * the schema calls "the ONE write path that turns skills, plugins and
220
+ * connectors into callable tools". Services are
221
+ * `GET /v1/integrations/connectors/providers` and connecting one is the device
222
+ * flow those provider cards declare.
223
+ *
224
+ * There is no "Plugins" row. `/v1/tools/plugins` reads like the next one and is
225
+ * an operator's inventory of what this deployment booted with — see the note in
226
+ * `tools.ts`. A row offering a reader subsystems they cannot switch is a row
227
+ * that does nothing.
228
+ *
229
+ * ONE PANEL, NOT FLYOUTS, and that is a fact about WHERE this menu is rather
230
+ * than a preference. The reference has "Use plugins ▸" opening a submenu, which
231
+ * works when the menu has screen to its side; this one hangs off a `+` at the
232
+ * right edge of a column that is itself at the right edge of the window.
233
+ * Measured at 1440: the sub-panel opened at x=1414 and is 190 wide, so 184px of
234
+ * it — every row in it — was off the screen, and `Menu.SubContent` publishes no
235
+ * placement to flip it with. Sections in one panel cannot fall off anything,
236
+ * and they put a skill one press away instead of two.
237
+ */
238
+ function Add({ at }) {
239
+ return (_jsxs(DropdownMenu, { placement: "bottom-end", maxHeight: 420, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Box, { render: "button", "aria-label": "Add to this conversation", p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16, "aria-hidden": true }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => take(at), children: _jsx(Word, { children: "Attach files" }) }), _jsx(DropdownMenuSeparator, {}), _jsx(Skills, {}), _jsx(DropdownMenuSeparator, {}), _jsx(Services, {})] })] }));
240
+ }
241
+ /**
242
+ * Asks for files and takes what is given.
243
+ *
244
+ * The picker is the browser's, minted and dropped per press rather than kept as
245
+ * a hidden input in the markup: a menu row is not a form control, and an
246
+ * `<input type=file>` parked in the tree is one more thing for a screen reader
247
+ * to walk past.
248
+ */
249
+ function take(at) {
250
+ if (typeof document === 'undefined')
251
+ return;
252
+ const ask = document.createElement('input');
253
+ ask.type = 'file';
254
+ ask.multiple = true;
255
+ ask.onchange = () => hold(at, Array.from(ask.files ?? []));
256
+ ask.click();
257
+ }
258
+ /**
259
+ * What this org's agents can reach for, as a section of this menu.
260
+ *
261
+ * A CHECKBOX ROW AND NOT A TICK IN A LABEL. `DropdownMenuCheckboxItem` is
262
+ * `role="menuitemcheckbox"` with `aria-checked`, so a screen reader says
263
+ * "checked" where a glyph in the text says nothing at all — and it draws the
264
+ * mark in gui's own indicator well, which a hand-rolled column beside the label
265
+ * cannot line up with. Activation is a two-state fact; this is the component
266
+ * for one.
267
+ */
268
+ function Skills() {
269
+ const { rows, trouble } = useSkills();
270
+ const [busy, setBusy] = useState(null);
271
+ const [on, setOn] = useState(null);
272
+ if (trouble)
273
+ return _jsx(Note, { children: trouble });
274
+ if (rows === null)
275
+ return _jsx(Note, { children: "Reading skills." });
276
+ if (rows.length === 0)
277
+ return _jsx(Note, { children: "No skills on this account yet." });
278
+ // The server's set once it has answered, and the list's own flag until then.
279
+ const isOn = (n, was) => (on ? on.has(n) : was);
280
+ return (_jsxs(_Fragment, { children: [_jsx(Note, { children: "Skills" }), rows.map((s) => {
281
+ const lit = isOn(s.name, s.activated);
282
+ return (_jsx(DropdownMenuCheckboxItem, { checked: lit,
283
+ // HELD OPEN, because switching two skills on is one errand and a
284
+ // menu that shuts on the first makes it two.
285
+ onSelect: (e) => e?.preventDefault(), onCheckedChange: () => {
286
+ setBusy(s.name);
287
+ // THE ANSWER IS THE WHOLE SET, so what comes back replaces what
288
+ // we hold rather than flipping our own flag: a refusal, a clamp,
289
+ // or another tab's change is visible in the reply and invisible
290
+ // in an optimistic toggle.
291
+ void activate(s.name, !lit)
292
+ .then((enabled) => setOn(new Set(enabled)))
293
+ .catch(() => { })
294
+ .finally(() => setBusy(null));
295
+ }, children: _jsx(Word, { children: busy === s.name ? `${s.name}…` : s.name }) }, s.name));
296
+ })] }));
297
+ }
298
+ /**
299
+ * What this account could connect.
300
+ *
301
+ * NARROWED TO THE ONE INTAKE THIS PANE CAN FINISH. A provider card declares its
302
+ * methods — `device`, `oauth`, `token` — and the device flow is the only one
303
+ * that completes without a redirect back into a page this column does not own.
304
+ * Offering the others here would be offering a press that goes nowhere; they
305
+ * are connected from the Directory, which is built for it.
306
+ */
307
+ function Services() {
308
+ const { rows, trouble } = useProviders();
309
+ const [step, setStep] = useState(null);
310
+ if (trouble)
311
+ return _jsx(Note, { children: trouble });
312
+ if (rows === null)
313
+ return _jsx(Note, { children: "Reading." });
314
+ const can = rows.filter((p) => p.methods.includes('device'));
315
+ if (can.length === 0)
316
+ return _jsx(Note, { children: "Nothing to connect from here yet." });
317
+ return (_jsxs(_Fragment, { children: [_jsx(Note, { children: step ?? 'Connect' }), can.map((p) => (_jsx(DropdownMenuItem, { onSelect: (e) => e?.preventDefault(), onClick: () => {
318
+ setStep(`Starting ${p.name}…`);
319
+ void startDevice(p.id)
320
+ .then((begun) => {
321
+ // The code is the reader's to type at the provider's page, so
322
+ // it is said here and the page is opened for them.
323
+ setStep(`Enter ${begun.userCode} at ${titleOf(begun.verifyUrl)}`);
324
+ window.open(begun.verifyUrl, '_blank', 'noopener,noreferrer');
325
+ })
326
+ .catch((e) => setStep(String(e?.message ?? 'Could not start')));
327
+ }, children: _jsx(Word, { children: p.name }) }, p.id)))] }));
328
+ }
329
+ /** The tab strip, the address, and whatever the chosen tab is showing. */
330
+ function Browser({ at, pane, onClose, }) {
331
+ const leaf = pane.tabs.find((t) => t.id === pane.at) ?? null;
332
+ // Bumping this remounts the frame, which is the only way to reload a document
333
+ // on an origin we cannot script into.
334
+ const [again, reload] = useState(0);
335
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack
336
+ // 44 IS THE FLOOR A THUMB NEEDS, and the strip scrolls rather than
337
+ // squeezing its tabs under it — the rule `tabs.tsx` exists to hold.
338
+ , {
339
+ // 44 IS THE FLOOR A THUMB NEEDS, and the strip scrolls rather than
340
+ // squeezing its tabs under it — the rule `tabs.tsx` exists to hold.
341
+ minH: 44, shrink: 0, items: "center", gap: "$1", px: "$2", overflow: "scroll", borderBottomWidth: 1, borderColor: "$borderColor", children: [pane.tabs.map((t) => (_jsx(Sheet, { at: at, leaf: t, on: t.id === pane.at }, t.id))), _jsx(Box, { render: "button", onClick: () => openTab(at, 'https://hanzo.ai'), "aria-label": "New tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16, "aria-hidden": true }) }), _jsx(Box, { flex: 1, minW: "$1" }), leaf ? (_jsxs(_Fragment, { children: [_jsx(Box, { render: "button", onClick: () => reload((n) => n + 1), "aria-label": "Reload this tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(RotateCw, { size: 16, "aria-hidden": true }) }), _jsx(Box, { render: "button", onClick: () => window.open(leaf.url, '_blank', 'noopener,noreferrer'), "aria-label": "Open this tab in a new window", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16, "aria-hidden": true }) })] })) : null, onClose ? (_jsx(Box, { render: "button", onClick: onClose, "aria-label": "Hide side panel", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(PanelRightClose, { size: 16, "aria-hidden": true }) })) : null] }), leaf ? _jsx(Where, { at: at, leaf: leaf }) : null, _jsx(YStack, { flex: 1, minH: 0, children: leaf ? (_jsx(Preview, { leaf: leaf, held: pane.held }, `${leaf.id}.${again}`)) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(Quiet, { children: "Nothing open here." }), _jsx(Quiet, { children: "Press an output or a source to look at it, or open a tab." })] })) })] }));
342
+ }
343
+ /** One tab: the thing you press, and the cross that shuts it. */
344
+ function Sheet({ at, leaf, on }) {
345
+ return (_jsxs(XStack, { items: "center", gap: "$1", pl: "$2", pr: "$1", minH: 32, maxW: 140, shrink: 0, rounded: "$2",
346
+ // A CHOSEN TAB IS THE ONLY ONE WITH AN OUTLINE. Everything else in the
347
+ // strip is a plain control that answers to hover, which is the rule the
348
+ // rest of this pane follows.
349
+ borderWidth: 1, borderColor: on ? '$borderColor' : 'transparent', bg: on ? '$raised' : 'transparent', hoverStyle: { bg: on ? '$raised' : '$hover' }, children: [_jsx(Box, { render: "button", onClick: () => pickTab(at, leaf.id), "aria-current": on, flex: 1, minW: 0, children: _jsx(Text, { fontSize: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, children: leaf.title }) }), _jsx(Box, { render: "button", onClick: () => shutTab(at, leaf.id), "aria-label": `Close ${leaf.title}`, p: "$1", rounded: "$1", hoverStyle: { bg: '$edge' }, children: _jsx(X, { size: 16, "aria-hidden": true }) })] }));
350
+ }
351
+ /**
352
+ * Where the open tab is, and how to go somewhere else.
353
+ *
354
+ * A PATH FOR A FILE AND A FIELD FOR A PAGE, because they are different
355
+ * questions: a blob has no address a reader can retype, so offering one is
356
+ * offering a control that cannot work. The field is the one place in this pane
357
+ * with an outline, which is what the house rule reserves an outline for.
358
+ */
359
+ function Where({ at, leaf }) {
360
+ const local = leaf.url.startsWith('blob:');
361
+ const [typed, setTyped] = useState(leaf.url);
362
+ if (local) {
363
+ return (_jsx(XStack, { px: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: leaf.title }) }));
364
+ }
365
+ return (_jsx(XStack, { items: "center", px: "$2", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: _jsx(XStack, { flex: 1, minW: 0, items: "center", px: "$2", minH: 28, rounded: "$2", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: _jsx(Text, { render: _jsx("input", {
366
+ // KEYED BY THE TAB at the call site above, so switching tabs starts
367
+ // this field at the new tab's address rather than carrying the old
368
+ // one's half-typed text across.
369
+ defaultValue: leaf.url, onChange: (e) => setTyped(e.target.value), onKeyDown: (e) => {
370
+ if (e.key === 'Enter')
371
+ openTab(at, address(typed), titleOf(address(typed)));
372
+ }, "aria-label": "Address", spellCheck: false }), width: "100%", borderWidth: 0, outlineStyle: "none", bg: "transparent", color: "var(--foreground)", fontSize: "$2" }) }) }));
373
+ }
374
+ /**
375
+ * What a tab is showing, or why it is not.
376
+ *
377
+ * A BROWSER CANNOT SHOW EVERY FILE AND SAYING SO IS THE FEATURE. An archive, a
378
+ * font, a binary — a frame pointed at one either downloads it or draws nothing,
379
+ * and drawing nothing is indistinguishable from a broken pane. The type comes
380
+ * off the held file where there is one, because a blob address carries no
381
+ * extension to guess from.
382
+ */
383
+ function Preview({ leaf, held }) {
384
+ const mine = held.find((h) => h.href === leaf.url);
385
+ const type = mine?.type ?? '';
386
+ const ext = suffix(mine?.name ?? leaf.title);
387
+ if (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg'].includes(ext)) {
388
+ return (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflow: "scroll", children: _jsx(View, { render: _jsx("img", { src: leaf.url, alt: leaf.title }), maxW: "100%", maxH: "100%", objectFit: "contain" }) }));
389
+ }
390
+ // THE LIST IS OF WHAT A FRAME CAN RENDER, not of what a file might be. A type
391
+ // absent from it is unsupported, which is the honest default: guessing that
392
+ // an unknown type will render is how a reader gets a blank rectangle.
393
+ const shows = !mine ||
394
+ type.startsWith('text/') ||
395
+ type === 'application/pdf' ||
396
+ type === 'application/json' ||
397
+ ['html', 'htm', 'txt', 'md', 'json', 'csv', 'pdf', 'css', 'js', 'ts'].includes(ext);
398
+ if (!shows) {
399
+ return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsxs(Quiet, { children: [(ext || type || 'This').toUpperCase(), " previews aren\u2019t supported yet."] }), _jsx(Quiet, { children: "Open this file in another app to view it." })] }));
400
+ }
401
+ return (_jsx(View, { render: _jsx("iframe", { src: leaf.url, title: leaf.title,
402
+ // TWO SANDBOXES, BECAUSE THERE ARE TWO KINDS OF DOCUMENT HERE AND ONE
403
+ // RULE WOULD BE WRONG FOR ONE OF THEM.
404
+ //
405
+ // A `blob:` address INHERITS THE ORIGIN OF THE PAGE THAT MINTED IT — this
406
+ // one. So a model's fenced block granted `allow-same-origin` would run as
407
+ // us, with our `localStorage`, which is where the IAM access token lives
408
+ // (`hanzo_iam_access_token`). An answer that wrote a script tag would be
409
+ // reading the reader's session. It gets `allow-scripts` so a generated
410
+ // page still lays out and runs, and never the pair.
411
+ //
412
+ // A remote page is the opposite case: the iframe takes the REMOTE
413
+ // origin, so `allow-same-origin` grants it nothing of ours and withholding
414
+ // it breaks the site — no cookies, no storage, no fonts on many hosts.
415
+ sandbox: leaf.url.startsWith('blob:')
416
+ ? 'allow-scripts allow-popups allow-forms'
417
+ : 'allow-same-origin allow-scripts allow-popups allow-forms' }), width: "100%", height: "100%", borderWidth: 0, bg: "var(--background)" }));
418
+ }
419
+ /** Shows or hides the summary above this conversation's browser. Exported for
420
+ * the conversation header, which is where the control belongs. */
421
+ export function usePinned() {
422
+ const open = useOpen();
423
+ const at = channel(open);
424
+ const pane = usePane(at);
425
+ return [pane.pinned, () => pin(at, !pane.pinned)];
426
+ }
package/dist/Room.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type Mode } from './Shell.js';
3
+ export declare function Room({ mode, front, children, }: {
4
+ mode: Mode;
5
+ /** Home at `/` on a workspace rooted there: a stranger meets the host's Landing (`Orgs`). */
6
+ front?: boolean;
7
+ children: ReactNode;
8
+ }): import("react").JSX.Element;
package/dist/Room.js ADDED
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect } from 'react';
4
+ import { Workspace } from './Shell.js';
5
+ import { Orgs } from './Orgs.js';
6
+ import { rememberDestination } from './lib/destination.js';
7
+ import { away } from './lib/host.js';
8
+ import { useHost } from './lib/hostname.js';
9
+ import { link } from './lib/tags.js';
10
+ import { useRooms } from './host.js';
11
+ export function Room({ mode, front = false, children, }) {
12
+ const { router } = useRooms();
13
+ // A sign-in leaves for the issuer and returns to a FIXED callback that knows
14
+ // nothing of the room the reader was in. So the room is written down here,
15
+ // the one place the app meets the router, and the trip `Orgs` starts for a
16
+ // signed-out reader returns HERE — address, `?q=` and all — rather than to
17
+ // the host's landing.
18
+ useEffect(() => {
19
+ rememberDestination();
20
+ }, []);
21
+ // THE APEX KEEPS THE APP AND THE WORKSPACE KEEPS ITS ROOMS. A room asked for
22
+ // on hanzo.ai is the same path and query on hanzo.team, and the location is
23
+ // replaced with it once React owns the page: the export is static, so nothing
24
+ // earlier knows the host. The room draws nothing while it leaves, so `Orgs`
25
+ // never starts a sign-in on the host being left.
26
+ const host = useHost();
27
+ const here = host ? window.location : null;
28
+ const to = here ? away(host, `${here.pathname}${here.search}${here.hash}`) : null;
29
+ useEffect(() => {
30
+ if (to)
31
+ window.location.replace(link(to));
32
+ }, [to]);
33
+ if (to)
34
+ return null;
35
+ // A session comes first, then the organization: nothing of the workspace is
36
+ // drawn until the reader has signed in at hanzo.id, belongs to an
37
+ // organization that is paid for, and has picked the one they are working in.
38
+ // Going to a room from the apex is going to hanzo.team, in this tab.
39
+ return (_jsx(Orgs, { front: front, children: _jsx(Workspace, { mode: mode, navigate: (route) => {
40
+ // A page of another origin — hanzo.ai's from hanzo.team — is a
41
+ // document load, not a client route the host's router can take.
42
+ if (/^https?:\/\//.test(route))
43
+ return window.location.assign(link(route));
44
+ const to = away(window.location.hostname, route);
45
+ if (to)
46
+ window.location.assign(link(to));
47
+ else
48
+ router.push(route);
49
+ }, back: () => router.back(), forward: () => router.forward(), children: children }) }));
50
+ }
@@ -0,0 +1,12 @@
1
+ export declare function Scene({ step, onNext, onSkip }: {
2
+ step: string;
3
+ onNext: () => void;
4
+ onSkip: () => void;
5
+ }): import("react").JSX.Element;
6
+ /** A stop's one primary control, the scene's and the card's. It takes focus when the stop opens, which is what
7
+ * `autoFocus` did on the element it replaced; a stop that only relabels it (Next to Done) keeps the focus it has.
8
+ */
9
+ export declare function Next({ onPress, label }: {
10
+ onPress: () => void;
11
+ label?: string;
12
+ }): import("react").JSX.Element;
package/dist/Scene.js ADDED
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The scene: one chat, the team brought in, and the call it turns into — played
4
+ // rather than read. It is drawn on a stage of its own, so nothing typed here
5
+ // reaches the composer, and nothing said here is sent or saved.
6
+ import { useEffect, useRef, useState } from 'react';
7
+ import { Text, View, XStack, YStack } from '@hanzo/gui';
8
+ import { Button } from '@hanzo/ui';
9
+ import { CAST as CREW } from './cast.js';
10
+ const CAST = {
11
+ you: { name: 'You', part: 'Owner', initial: 'Y', ground: 'var(--neutral-700)' },
12
+ des: { name: 'Des', part: 'Designer', face: '/agents/des.png', initial: 'D', ground: CREW.des.ground },
13
+ vi: { name: 'Vi', part: 'Visionary', face: '/agents/vi.png', initial: 'V', ground: CREW.vi.ground },
14
+ alex: { name: 'Alex', part: 'Teammate', initial: 'A', ground: 'var(--neutral-600)' },
15
+ };
16
+ /** The tour's own prompt: typed for show, never sent. */
17
+ const SAMPLE = 'Help me plan the next version of our app';
18
+ const ADDED = ['des', 'vi', 'alex'];
19
+ const CHAT = [
20
+ { who: 'you', text: 'We need to create a more immersive experience.' },
21
+ { who: 'vi', text: 'Should we create UI for wearable glasses?' },
22
+ { who: 'des', text: 'I love this idea. We could explore the best settings in Roblox and other top games and create some of the best ideas!' },
23
+ { who: 'alex', text: "What a great idea, let's jump on a call now so we can plan and create deliverables and due dates." },
24
+ ];
25
+ const CALL = [
26
+ { who: 'alex', text: "Let's plan it. Vi, you own the concept. Des, the first mockups. I'll set the deliverables and due dates." },
27
+ { who: 'vi', text: 'First deliverable: a glasses-ready layout, due Friday.' },
28
+ { who: 'des', text: "I'll pull references from the top Roblox worlds and draft three directions, due Wednesday." },
29
+ { who: 'you', text: 'Perfect. Save the transcript and put the deliverables on the Board.' },
30
+ ];
31
+ // The beats, in order: the prompt, three people added, four lines of chat, the
32
+ // move to a call, four lines said on it, and the record of it.
33
+ const PROMPT = 0;
34
+ const ADD = 1; // .. 3
35
+ const TALK = 4; // .. 7
36
+ const RING = 8;
37
+ const SAY = 9; // .. 12
38
+ const SAVED = 13;
39
+ const MS = 26;
40
+ /** Text revealed a character at a time; says when it is all there. */
41
+ function Typed({ text, go, onDone }) {
42
+ const [n, setN] = useState(go ? 0 : text.length);
43
+ useEffect(() => {
44
+ if (!go)
45
+ return;
46
+ if (n >= text.length) {
47
+ const t = window.setTimeout(onDone, 520);
48
+ return () => window.clearTimeout(t);
49
+ }
50
+ const t = window.setTimeout(() => setN((k) => k + 1), MS);
51
+ return () => window.clearTimeout(t);
52
+ }, [go, n, text, onDone]);
53
+ return _jsx(_Fragment, { children: text.slice(0, n) });
54
+ }
55
+ function Face({ who, size = 28 }) {
56
+ const c = CAST[who];
57
+ return (_jsx(View, { render: "span", "aria-hidden": true, width: size, height: size, rounded: 9999, bg: c.ground, display: "inline-flex", items: "center", justify: "center", overflow: "hidden", shrink: 0, children: c.face ? (_jsx("img", { src: c.face, alt: "", width: size, height: size })) : (_jsx(Text, { fontFamily: "$body", fontSize: size * 0.42, fontWeight: "600", color: "var(--pure-white)", children: c.initial })) }));
58
+ }
59
+ const ink = '$ink';
60
+ const dim = '$soft';
61
+ const line = '$borderColor';
62
+ // The stage is portalled to <body> by the Guide, outside every font scope gui
63
+ // writes, so a `$N` size or leading resolves only on a Text that names `$body`
64
+ // itself — which is why every Text below does.
65
+ /** A line of the scene's own type: the base rung on its own leading. */
66
+ const SAID = { fontSize: '$3', lineHeight: '$3' };
67
+ export function Scene({ step, onNext, onSkip }) {
68
+ const [beat, setBeat] = useState(PROMPT);
69
+ const [typed, setTyped] = useState(0);
70
+ const advance = () => setBeat((b) => Math.min(b + 1, SAVED));
71
+ const settle = () => setTyped((k) => k + 1);
72
+ // Beats that type advance themselves when the typing is done; the ones that
73
+ // only appear advance on a short pause.
74
+ useEffect(() => {
75
+ const typing = beat === PROMPT || (beat >= TALK && beat < RING) || (beat >= SAY && beat < SAVED);
76
+ if (typing)
77
+ return;
78
+ if (beat === SAVED)
79
+ return;
80
+ const t = window.setTimeout(advance, beat === RING ? 1400 : 900);
81
+ return () => window.clearTimeout(t);
82
+ }, [beat]);
83
+ useEffect(() => {
84
+ if (typed === 0)
85
+ return;
86
+ advance();
87
+ }, [typed]);
88
+ const added = ADDED.slice(0, Math.max(0, Math.min(ADDED.length, beat - ADD + 1)));
89
+ const said = CHAT.slice(0, Math.max(0, Math.min(CHAT.length, beat - TALK + 1)));
90
+ const onCall = beat >= RING;
91
+ const spoken = CALL.slice(0, Math.max(0, Math.min(CALL.length, beat - SAY + 1)));
92
+ const speaker = onCall && beat >= SAY && beat < SAVED ? CALL[beat - SAY].who : null;
93
+ return (_jsxs(YStack, { role: "group", "aria-label": "A chat that becomes a call", position: "fixed", l: "50%", t: "50%", x: "-50%", y: "-50%", width: "min(720px, calc(100vw - 32px))", maxH: "calc(100vh - 32px)", overflowY: "auto", z: "var(--z-modal)", bg: "var(--popover)", borderWidth: 1, borderColor: "var(--border-control)", rounded: 14, boxShadow: "var(--shadow-floating)", p: 18, children: [_jsxs(XStack, { items: "center", gap: 10, mb: 12, children: [_jsx(Text, { fontFamily: "$body", fontSize: 10, letterSpacing: 1.1, textTransform: "uppercase", color: "$faint", children: step }), _jsx(View, { flex: 1, height: 1, bg: line }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: dim, children: onCall ? 'On a call' : 'In chat' })] }), _jsx(Text, { fontFamily: "$body", render: "h2", mb: 4, fontSize: "$6", fontWeight: "600", lineHeight: "$4", color: ink, children: "More than chat" }), _jsx(Text, { fontFamily: "$body", render: "p", mb: 14, ...SAID, color: "$quiet", children: "Start with a bot, bring in the team, and take it to a call. Nothing here is sent or saved." }), !onCall ? (_jsxs(YStack, { borderWidth: 1, borderColor: line, rounded: 12, overflow: "hidden", children: [_jsxs(XStack, { items: "center", gap: 10, px: 12, py: 10, borderBottomWidth: 1, borderColor: line, children: [_jsx(Face, { who: "des" }), _jsxs(YStack, { children: [_jsx(Text, { fontFamily: "$body", fontSize: "$3", fontWeight: "600", lineHeight: "$1", color: ink, children: "des" }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", lineHeight: "$1", color: dim, children: "Designer \u00B7 bot" })] }), _jsx(View, { flex: 1 }), _jsxs(XStack, { gap: 6, flexWrap: "wrap", justify: "flex-end", children: [added.map((w) => (_jsxs(XStack, { items: "center", gap: 6, pl: 4, pr: 8, py: 3, rounded: 999, borderWidth: 1, borderColor: line, children: [_jsx(Face, { who: w, size: 18 }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: ink, children: CAST[w].name }), _jsxs(Text, { fontFamily: "$body", fontSize: "$2", color: dim, children: ["\u00B7 ", CAST[w].part] })] }, w))), beat >= ADD && beat < TALK ? (_jsx(Text, { fontFamily: "$body", px: 8, py: 3, rounded: 999, borderWidth: 1, borderStyle: "dashed", borderColor: line, fontSize: "$2", color: dim, children: "+ Add to chat" })) : null] })] }), _jsxs(View, { display: "grid", p: 12, gap: 10, minH: 168, children: [said.map((m, k) => (_jsxs(XStack, { gap: 10, items: "flex-start", flexDirection: m.who === 'you' ? 'row-reverse' : 'row', children: [_jsx(Face, { who: m.who }), _jsxs(YStack, { maxW: "78%", px: 12, py: 8, rounded: 12, bg: m.who === 'you' ? 'var(--foreground)' : '$hover', children: [m.who !== 'you' ? (_jsx(Text, { fontFamily: "$body", fontSize: "$1", color: dim, mb: 2, children: CAST[m.who].name })) : null, _jsx(Text, { fontFamily: "$body", ...SAID, color: m.who === 'you' ? 'var(--background)' : ink, children: _jsx(Typed, { text: m.text, go: k === said.length - 1 && beat < RING, onDone: settle }) })] })] }, k))), said.length === 0 ? (_jsx(Text, { fontFamily: "$body", fontSize: "$2", color: dim, text: "center", pt: 48, children: beat < ADD ? 'A chat with des, your designer bot.' : 'Bringing the team in…' })) : null] }), _jsxs(Text, { fontFamily: "$body", m: 12, px: 12, py: 10, rounded: 10, borderWidth: 1, borderColor: line, ...SAID, color: beat === PROMPT ? ink : dim, minH: 40, children: [beat === PROMPT ? _jsx(Typed, { text: SAMPLE, go: true, onDone: settle }) : beat < TALK ? SAMPLE : 'Ask anything', _jsx(View, { render: "span", "aria-hidden": true, display: beat === PROMPT ? 'inline-flex' : 'none', width: 1, height: 14, bg: ink, ml: 1, y: 2 })] })] })) : (_jsxs(YStack, { borderWidth: 1, borderColor: line, rounded: 12, overflow: "hidden", children: [_jsx(View, { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, p: 12, borderBottomWidth: 1, borderColor: line, children: ['you', 'des', 'vi', 'alex'].map((w) => (_jsxs(YStack, { items: "center", gap: 6, px: 6, py: 12, rounded: 10, bg: "$hover", outlineWidth: 2, outlineStyle: "solid", outlineColor: speaker === w ? ink : 'transparent', transition: "200ms", children: [_jsx(Face, { who: w, size: 40 }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: ink, children: CAST[w].name })] }, w))) }), _jsxs(YStack, { p: 12, minH: 168, children: [_jsx(Text, { fontFamily: "$body", fontSize: "$1", letterSpacing: 1.1, textTransform: "uppercase", color: dim, mb: 8, children: beat === RING ? 'Calling…' : 'Live transcript' }), _jsx(View, { display: "grid", gap: 8, children: spoken.map((m, k) => (_jsxs(XStack, { gap: 8, children: [_jsx(Text, { fontFamily: "$body", ...SAID, color: dim, shrink: 0, width: 44, children: CAST[m.who].name }), _jsx(Text, { fontFamily: "$body", ...SAID, color: ink, children: _jsx(Typed, { text: m.text, go: k === spoken.length - 1 && beat < SAVED, onDone: settle }) })] }, k))) }), beat === SAVED ? (_jsx(Text, { fontFamily: "$body", mt: 14, px: 10, py: 8, rounded: 8, borderWidth: 1, borderColor: line, fontSize: "$2", color: ink, children: "\u2713 Transcript saved \u00B7 Deliverables and due dates on the Board" })) : null] })] })), _jsxs(XStack, { items: "center", gap: 8, mt: 14, children: [_jsx(Button, { variant: "linkMuted", size: "sm", px: 2, onClick: onSkip, children: "Skip" }), _jsx(View, { flex: 1 }), beat === SAVED ? (_jsx(Button, { size: "sm", onClick: () => {
94
+ setTyped(0);
95
+ setBeat(PROMPT);
96
+ }, children: "Replay" })) : null, _jsx(Next, { onPress: onNext })] })] }));
97
+ }
98
+ /** A stop's one primary control, the scene's and the card's. It takes focus when the stop opens, which is what
99
+ * `autoFocus` did on the element it replaced; a stop that only relabels it (Next to Done) keeps the focus it has.
100
+ */
101
+ export function Next({ onPress, label = 'Next' }) {
102
+ const ref = useRef(null);
103
+ useEffect(() => ref.current?.focus(), []);
104
+ return (_jsx(Button, { ref: ref, variant: "primary", size: "sm", onClick: onPress, children: label }));
105
+ }