@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,15 @@
1
+ import { type Host } from '@hanzo/build';
2
+ import type { Mode } from './recent.js';
3
+ export declare function Column({ host, mode, thread, onChat, onDev, go, drawer, }: {
4
+ host: Host;
5
+ mode: Mode;
6
+ /** The Chat conversation the pane holds, or null. */
7
+ thread: string | null;
8
+ /** The switch: to Chat and to Dev, each where the reader left it. */
9
+ onChat: () => void;
10
+ onDev: () => void;
11
+ /** Moves the app to an address of this page. */
12
+ go: (address: string) => void;
13
+ /** Drawn in the phone's drawer: no collapse, no edge, always open. */
14
+ drawer?: boolean;
15
+ }): import("react").JSX.Element;
package/dist/Column.js ADDED
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The app's one column, the same in both modes: the Chat / Dev switch, a filter,
4
+ // New, Dev's places, the Recents of the mode it is in, and at the foot the
5
+ // Slack card and the account (components/workspace/Me.tsx).
6
+ //
7
+ // It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
8
+ // gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
9
+ // `Grip`), kept per browser. Pulled in past its floor it collapses to a rail of
10
+ // marks, kept under the key the builder's rail kept it under, so a reader's
11
+ // choice outlives the move.
12
+ import { Text, XStack, YStack } from '@hanzo/gui';
13
+ import { ChevronDown, ChevronUp, PanelLeft, Plus, Search } from 'lucide-react';
14
+ import { Grip, nav, path, route, Slack } from '@hanzo/build';
15
+ import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
16
+ import { HanzoMark } from '@hanzogui/shell';
17
+ import { useState } from 'react';
18
+ import { ModeMark } from './ModeMark.js';
19
+ import { useKept } from './kept.js';
20
+ import { Made, Me } from './Me.js';
21
+ import { Recents } from './Recents.js';
22
+ import { Setup } from './Setup.js';
23
+ import { empty } from './open.js';
24
+ import { useSpan } from './span.js';
25
+ import { pane } from './ground.js';
26
+ import { pick } from './lib/session.js';
27
+ import { app, talk } from './lib/host.js';
28
+ /** The grip stands in the gutter between the column and the pane. gui carries a `calc()` to the page unchanged; its
29
+ * types only name custom properties. */
30
+ const GUTTER_END = 'calc(-1 * var(--pane-gap))';
31
+ /** The sidebar fills the column. Its `width` is typed in px alone, though its frame takes any gui size — the type is
32
+ * @hanzo/ui's to widen. */
33
+ const FILL = { width: '100%' };
34
+ /** The column's width, px: its default, and the two a drag holds it between. */
35
+ const WIDTH = 272;
36
+ const FLOOR = 220;
37
+ const CEIL = 420;
38
+ /** The collapsed rail's width, px. */
39
+ const RAIL = 60;
40
+ /** Per reader, per browser. */
41
+ const SPAN = 'hanzo.app.column';
42
+ /** Enter and Space press a row the way a click does. */
43
+ const keys = (press) => ({
44
+ onKeyDown: (e) => {
45
+ if (e.key !== 'Enter' && e.key !== ' ')
46
+ return;
47
+ e.preventDefault?.();
48
+ press();
49
+ },
50
+ });
51
+ /** A row of the column: a pill, lit where the reader is. */
52
+ const ROW = { rounded: '$4', minH: 34 };
53
+ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
54
+ const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
55
+ const collapsed = kept && !drawer;
56
+ const [slack, setSlack] = useKept('hanzo.build.slack', false);
57
+ const [unfolded, setUnfolded] = useState(false);
58
+ const [words, setWords] = useState('');
59
+ const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
60
+ const dev = (p) => host.go(p);
61
+ const r = route(host.path);
62
+ const fresh = mode === 'chat' ? !thread : r.kind === 'new';
63
+ const { links, more } = nav(host, dev);
64
+ const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
65
+ const create = () => {
66
+ if (mode === 'dev')
67
+ return dev('');
68
+ empty();
69
+ go(talk());
70
+ };
71
+ // Another organization: picked, and the app opened in it at the mode's New.
72
+ const org = (o) => {
73
+ pick(o);
74
+ window.location.assign(mode === 'dev' ? app('') : talk());
75
+ };
76
+ if (collapsed)
77
+ return (_jsx(YStack, { "data-slot": "app-column", "data-collapsed": "", position: "relative", shrink: 0, width: RAIL, "$max-md": { display: 'none' }, children: _jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "true", role: "navigation", "aria-label": "Hanzo", ...pane(), ...FILL, px: "$1.5", py: "$2.5", gap: "$1.5", items: "center", children: [_jsx(SidebarIconButton, { label: "Expand sidebar", onPress: () => setCollapsed(false), width: 36, height: 36, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev, stacked: true }), _jsx(SidebarIconButton, { label: mode === 'chat' ? 'New chat' : 'New run', onPress: create, width: 36, height: 36, rounded: 999, children: _jsx(Plus, { size: 16 }) }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: org })] }) }));
78
+ return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
79
+ // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
80
+ style: drawer ? undefined : span.style, position: "relative", shrink: 0, height: "100%", width: drawer ? '100%' : 'var(--span)', ...(drawer ? {} : { '$max-md': { display: 'none' } }), children: [_jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "false", role: "navigation", "aria-label": "Hanzo", ...pane(), ...FILL, px: "$2.5", pt: "$3", pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "mode-switch", items: "center", gap: "$2", px: "$1.5", pb: "$3", minW: 0, children: [_jsx(HanzoMark, { size: 18 }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev })] }), _jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", px: "$3", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
81
+ if (e.key === 'Escape')
82
+ setWords('');
83
+ }, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
84
+ ? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
85
+ : null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {}), host.person && !slack ? (_jsx(YStack, { "data-slot": "app-slack", children: _jsx(Slack, { onOpen: () => dev(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) }) })) : null] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
86
+ // In the gutter between the column and the pane, not over the column's own rows.
87
+ r: GUTTER_END, width: "var(--pane-gap)" }))] }));
88
+ }
@@ -0,0 +1 @@
1
+ export declare function Contacts(): import("react").JSX.Element;
@@ -0,0 +1,301 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The directory: who is in this org, and who you can put in a room.
4
+ //
5
+ // IT IS A LIST OF SUBJECTS AND NOTHING ELSE. People, the org's agents, and the
6
+ // presets an org can install as one. It holds no conversations, keeps no
7
+ // threads, and remembers nothing of its own — the rooms it starts live in the
8
+ // thread store with every other conversation, which is `conversations.ts`.
9
+ //
10
+ // WHAT IT USED TO DO. Four `localStorage` keys: a roster of people it invented,
11
+ // a list of agents it invented, a set of `direct_…`/`group_…`/`call_…` threads
12
+ // nothing on the platform had ever seen, and an event the sidebar merged them
13
+ // in through. Adding a person wrote a row into that store — no invitation, no
14
+ // account, nobody told — and the row was gone on the next machine. Pressing
15
+ // Call minted a thread whose title began with a telephone and rang nowhere.
16
+ //
17
+ // EVERY ROW IS NOW A READ, and every control a write:
18
+ // people GET /v1/iam/scim/v2/Users?owner=<org> (`usePeople`)
19
+ // agents GET /v1/agents (`useCrew`, crew.tsx)
20
+ // presets GET /v1/agent/chat/presets (the catalog, public)
21
+ // invite POST /v1/iam/invitations (`createInvitation`)
22
+ // agent POST /v1/agents (`Hire`, the crew's sheet)
23
+ // room POST /v1/agents/chat/conversations (`start`)
24
+ //
25
+ // THERE IS NO CALL CONTROL, because there is no call. Measured against the
26
+ // platform's own contract — 1629 paths in /v1/openapi.json — nothing serves
27
+ // signalling, an SFU, a room token or a TURN credential. A button that says
28
+ // Call and opens a chat is worse than no button: it reports a thing that did
29
+ // not happen. When the estate serves a call, the room it attaches to is the one
30
+ // `start` already writes, and this file gains a verb rather than a store.
31
+ import { useCallback, useEffect, useMemo, useState } from 'react';
32
+ import { useRooms } from './host.js';
33
+ import { Check, Plus, Search, UserPlus } from 'lucide-react';
34
+ import { Box, Text, XStack, YStack } from '@hanzo/ui';
35
+ import { useAi, useModels, usePeople, useThreads } from '@hanzo/ai/react';
36
+ import { useIam, useOrganizations } from '@hanzo/iam/react';
37
+ import { hasSession } from './lib/session.js';
38
+ import { useHydrated } from './lib/hydrated.js';
39
+ import { ENSO, served } from './lib/ai.js';
40
+ import { createInvitation, inviteLink } from './lib/invite.js';
41
+ import { reach } from './lib/reach.js';
42
+ import { Face } from './cast.js';
43
+ import { Take } from './copy.js';
44
+ import { Hire, useCrew } from './crew.js';
45
+ import { called } from './team.js';
46
+ import { say } from './failure.js';
47
+ import { openThread } from './open.js';
48
+ import { spokenAt, start } from "./conversations.js";
49
+ const TABS = [
50
+ { id: 'all', label: 'All' },
51
+ { id: 'person', label: 'People' },
52
+ { id: 'agent', label: 'Agents' },
53
+ { id: 'preset', label: 'Presets' },
54
+ ];
55
+ export function Contacts() {
56
+ const hydrated = useHydrated();
57
+ const { isAuthenticated } = useIam();
58
+ const { currentOrgId } = useOrganizations();
59
+ const signedIn = isAuthenticated || hasSession();
60
+ // A prerender knows neither the reader nor the org, so it draws the heading
61
+ // and waits. Announcing "sign in" before hydration tells a signed-in reader
62
+ // they are signed out for a frame.
63
+ if (!hydrated)
64
+ return _jsx(Alone, { children: "\u00A0" });
65
+ if (!signedIn)
66
+ return _jsx(Alone, { children: "Sign in to see who is in your organization." });
67
+ if (!currentOrgId)
68
+ return _jsx(Alone, { children: "This account is not in an organization yet." });
69
+ return _jsx(Roster, { owner: currentOrgId });
70
+ }
71
+ /** The room's one heading, for every state that has nothing to list. */
72
+ function Alone({ children }) {
73
+ return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$3", children: [_jsx(Text, { render: "h1", fontSize: "$4", fontWeight: "600", color: "$ink", children: "Contacts" }), _jsx(Text, { fontSize: "$3", color: "$soft", maxW: 420, text: "center", children: children })] }));
74
+ }
75
+ /** A read that answered no, said once, above whatever else is on screen. */
76
+ function Note({ children }) {
77
+ return (_jsx(Text, { fontSize: "$2", color: "$soft", px: "$3", py: "$2", children: children }));
78
+ }
79
+ /**
80
+ * The directory itself.
81
+ *
82
+ * SPLIT OUT because `usePeople` needs an owner and a hook cannot be called
83
+ * conditionally — the same reason Home and Vibe split theirs.
84
+ */
85
+ function Roster({ owner }) {
86
+ const ai = useAi();
87
+ const { router } = useRooms();
88
+ const { people, loading: reading, error: unread } = usePeople({ owner, service: false });
89
+ // `create` and not `ai.agents.create`: the hook's own writer refreshes the
90
+ // list it read, which is what moves an installed preset out of the catalog
91
+ // and into the org's agents without a reload.
92
+ const { agents, create, loading: gathering, error: ungathered } = useCrew();
93
+ // WHAT "RECENTLY" MEANS: the conversations this reader has. It is the same
94
+ // read the sidebar makes, and the only record of who a room was with.
95
+ const { threads } = useThreads();
96
+ // An installed preset needs a model, and `POST /v1/agents` refuses one this
97
+ // deployment does not serve — so it is named against the served catalog by
98
+ // the one function that does that, rather than typed in and hoped for.
99
+ const { models } = useModels();
100
+ // `null` is the catalog unknown; `[]` is a deployment that publishes none.
101
+ const [presets, setPresets] = useState(null);
102
+ const [unlisted, setUnlisted] = useState(null);
103
+ useEffect(() => {
104
+ const stop = new AbortController();
105
+ ai.http
106
+ .collection('presets', {
107
+ method: 'GET',
108
+ path: '/v1/agent/chat/presets',
109
+ signal: stop.signal,
110
+ })
111
+ .then(setPresets)
112
+ .catch((e) => {
113
+ if (!stop.signal.aborted)
114
+ setUnlisted(e);
115
+ });
116
+ return () => stop.abort();
117
+ }, [ai]);
118
+ const [tab, setTab] = useState('all');
119
+ const [order, setOrder] = useState('name');
120
+ const [look, setLook] = useState('');
121
+ const [picked, setPicked] = useState([]);
122
+ const [inviting, setInviting] = useState(false);
123
+ const [hiring, setHiring] = useState(false);
124
+ const [busy, setBusy] = useState(null);
125
+ const [wrong, setWrong] = useState(null);
126
+ const entries = useMemo(() => {
127
+ const rows = [];
128
+ for (const one of people) {
129
+ const label = one.displayName || one.name;
130
+ rows.push({
131
+ key: `person:${one.id}`,
132
+ kind: 'person',
133
+ name: one.name,
134
+ label,
135
+ note: one.email ?? '',
136
+ at: spokenAt(threads, label),
137
+ });
138
+ }
139
+ for (const one of agents) {
140
+ rows.push({
141
+ key: `agent:${one.id}`,
142
+ kind: 'agent',
143
+ name: one.name,
144
+ label: called(one.name),
145
+ note: one.description || one.model || '',
146
+ avatar: one.avatar,
147
+ emoji: one.emoji,
148
+ at: spokenAt(threads, one.name),
149
+ });
150
+ }
151
+ // A preset the org has already installed IS the agent above it. Drawing
152
+ // both would offer to install what is already here.
153
+ for (const one of presets ?? []) {
154
+ if (agents.some((made) => made.name === one.id))
155
+ continue;
156
+ rows.push({
157
+ key: `preset:${one.id}`,
158
+ kind: 'preset',
159
+ name: one.id,
160
+ label: one.title || one.id,
161
+ note: 'Not installed',
162
+ at: 0,
163
+ });
164
+ }
165
+ return rows;
166
+ }, [people, agents, presets, threads]);
167
+ const shown = useMemo(() => {
168
+ const q = look.trim().toLowerCase();
169
+ return entries
170
+ .filter((one) => tab === 'all' || one.kind === tab)
171
+ .filter((one) => !q || `${one.label} ${one.name} ${one.note}`.toLowerCase().includes(q))
172
+ .sort((a, b) => order === 'name'
173
+ ? a.label.localeCompare(b.label)
174
+ : b.at - a.at || a.label.localeCompare(b.label));
175
+ }, [entries, tab, look, order]);
176
+ const install = useCallback(async (one) => {
177
+ const preset = (presets ?? []).find((p) => p.id === one.name);
178
+ if (!preset || busy)
179
+ return;
180
+ setBusy(one.key);
181
+ setWrong(null);
182
+ try {
183
+ // The list this row leaves and the list it joins are both `useAgents`,
184
+ // so the row moves by itself.
185
+ await create({
186
+ name: preset.id,
187
+ model: served(models, ENSO),
188
+ ...(preset.systemPrompt ? { instructions: preset.systemPrompt } : {}),
189
+ ...(preset.title ? { description: preset.title } : {}),
190
+ });
191
+ }
192
+ catch (e) {
193
+ setWrong(reach(e));
194
+ }
195
+ finally {
196
+ setBusy(null);
197
+ }
198
+ }, [busy, create, models, presets]);
199
+ /**
200
+ * INTO A ROOM, by the one call that starts one.
201
+ *
202
+ * One selected is a direct conversation, several is a group, and a group of
203
+ * agents or a mix of both is the same act — nothing here branches on how many
204
+ * or on what kind, because the thread store does not either.
205
+ */
206
+ const enter = useCallback(async () => {
207
+ const members = picked
208
+ .map((key) => entries.find((one) => one.key === key))
209
+ .filter((one) => Boolean(one))
210
+ .map((one) => ({ id: one.key, name: one.label || one.name, agent: one.kind === 'agent' }));
211
+ if (members.length === 0 || busy)
212
+ return;
213
+ setBusy('room');
214
+ setWrong(null);
215
+ try {
216
+ const id = await start(ai, members);
217
+ // The selection the chat pane reads, and the address a reload lands on.
218
+ openThread(id);
219
+ router.push(`/chat?thread=${encodeURIComponent(id)}`);
220
+ }
221
+ catch (e) {
222
+ setWrong(reach(e));
223
+ }
224
+ finally {
225
+ setBusy(null);
226
+ }
227
+ }, [ai, busy, entries, picked, router]);
228
+ const waiting = (reading && people.length === 0) || (gathering && agents.length === 0);
229
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { height: 48, shrink: 0, items: "center", gap: "$2", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { render: "h1", flex: 1, fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, children: "Contacts" }), _jsx(Control, { onPress: () => setInviting(true), icon: _jsx(UserPlus, { size: 13, "aria-hidden": true }), children: "Invite" }), _jsx(Control, { onPress: () => setHiring(true), icon: _jsx(Plus, { size: 13, "aria-hidden": true }), children: "New agent" })] }), _jsxs(XStack, { shrink: 0, items: "center", gap: "$2", px: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsxs(XStack, { items: "center", gap: "$1.5", flex: 1, minW: 160, px: "$2", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", children: [_jsx(Search, { size: 13, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: look, onChange: (e) => setLook(e.target.value), placeholder: "Search", "aria-label": "Search contacts" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2", px: 0, py: 6 })] }), TABS.map((one) => (_jsx(Pill, { on: tab === one.id, onPress: () => setTab(one.id), children: one.label }, one.id))), _jsx(Box, { width: 1, height: 18, bg: "$borderColor" }), _jsx(Pill, { on: order === 'name', onPress: () => setOrder('name'), children: "A\u2013Z" }), _jsx(Pill, { on: order === 'recent', onPress: () => setOrder('recent'), children: "Recent" })] }), unread ? _jsx(Note, { children: say(unread, 'your organization') }) : null, ungathered ? _jsx(Note, { children: say(ungathered, "this org's agents") }) : null, unlisted ? _jsx(Note, { children: say(unlisted, 'the agent presets') }) : null, wrong ? _jsx(Note, { children: wrong }) : null, _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", children: waiting && shown.length === 0 ? (_jsx(Note, { children: "Reading your directory." })) : shown.length === 0 ? (_jsx(Note, { children: look.trim()
230
+ ? `Nobody here matches “${look.trim()}”.`
231
+ : tab === 'agent'
232
+ ? 'No agents yet. New agent writes one.'
233
+ : tab === 'preset'
234
+ ? 'Every preset this deployment publishes is installed.'
235
+ : 'Nobody else in this organization yet. Invite sends the link.' })) : (shown.map((one) => (_jsx(Row, { one: one, on: picked.includes(one.key), busy: busy === one.key, onPress: () => one.kind === 'preset'
236
+ ? void install(one)
237
+ : setPicked((was) => was.includes(one.key)
238
+ ? was.filter((k) => k !== one.key)
239
+ : [...was, one.key]) }, one.key)))) }), picked.length > 0 ? (_jsxs(XStack, { shrink: 0, items: "center", gap: "$2", p: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { flex: 1, fontSize: "$2", color: "$soft", numberOfLines: 1, children: picked.length === 1 ? '1 selected' : `${picked.length} selected` }), _jsx(Control, { onPress: () => setPicked([]), children: "Clear" }), _jsx(Control, { onPress: () => void enter(), strong: true, children: busy === 'room' ? 'Opening…' : 'Start conversation' })] })) : null, inviting ? _jsx(Invite, { owner: owner, onClose: () => setInviting(false) }) : null, hiring ? _jsx(Hire, { onClose: () => setHiring(false), onMade: () => setHiring(false) }) : null] }));
240
+ }
241
+ /** One row: a face, a name, a line about them, and what pressing it does. */
242
+ function Row({ one, on, busy, onPress, }) {
243
+ return (_jsx(Box, { render: "button", onClick: onPress, "aria-label": one.label, "aria-pressed": one.kind === 'preset' ? undefined : on, width: "100%", display: "block", cursor: "pointer", borderWidth: 0, borderBottomWidth: 1, borderBottomColor: "var(--borderColor, var(--white-08))", bg: on ? 'var(--raised, var(--white-06))' : 'transparent', children: _jsxs(XStack, { gap: "$3", items: "center", p: "$3", children: [_jsx(Face, { src: one.avatar, emoji: one.emoji, name: one.name, size: 32 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, text: "left", children: one.label }), one.note ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, text: "left", children: one.note })) : null] }), one.kind === 'preset' ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: busy ? 'Installing…' : 'Install' })) : on ? (_jsx(Check, { size: 14, "aria-hidden": true })) : null] }) }));
244
+ }
245
+ /** A named control. One shape for every button in this room. */
246
+ function Control({ children, onPress, icon, strong, }) {
247
+ return (_jsx(Box, { render: "button", onClick: onPress, px: "$3", py: "$2", rounded: "$3", borderWidth: strong ? 0 : 1, borderColor: "$borderColor", bg: strong ? '$ink' : 'transparent', hoverStyle: { bg: strong ? '$ink' : '$hover' }, cursor: "pointer", children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [icon, _jsx(Text, { fontSize: "$2", color: strong ? '$background' : '$ink', fontWeight: strong ? '600' : undefined, numberOfLines: 1, children: children })] }) }));
248
+ }
249
+ /** A switch that is on or off — a tab, an order. Same shape, so they read alike. */
250
+ function Pill({ children, on, onPress, }) {
251
+ return (_jsx(Box, { render: "button", onClick: onPress, "aria-pressed": on, px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, children: children }) }));
252
+ }
253
+ /**
254
+ * ADDING A PERSON IS AN INVITATION, and it is the only honest shape for it.
255
+ *
256
+ * The other way in is `POST /v1/iam/users`, which takes the password the person
257
+ * will sign in with — so adding a colleague would mean choosing their
258
+ * credential and then telling it to them. An invitation is a code they redeem
259
+ * on hanzo.id under a password only they ever see.
260
+ *
261
+ * The link is the deliverable. Nothing here sends mail: no route on the
262
+ * platform mails an invitation, so this hands over the address instead of
263
+ * claiming to have sent one.
264
+ */
265
+ function Invite({ owner, onClose }) {
266
+ const [email, setEmail] = useState('');
267
+ const [link, setLink] = useState(null);
268
+ const [busy, setBusy] = useState(false);
269
+ const [wrong, setWrong] = useState(null);
270
+ useEffect(() => {
271
+ const key = (e) => {
272
+ if (e.key === 'Escape')
273
+ onClose();
274
+ };
275
+ window.addEventListener('keydown', key);
276
+ return () => window.removeEventListener('keydown', key);
277
+ }, [onClose]);
278
+ const make = async () => {
279
+ if (busy)
280
+ return;
281
+ setBusy(true);
282
+ setWrong(null);
283
+ try {
284
+ const invitation = await createInvitation(owner, email.trim() || undefined);
285
+ setLink(inviteLink(invitation.owner, invitation.code));
286
+ }
287
+ catch (e) {
288
+ setWrong(reach(e));
289
+ }
290
+ finally {
291
+ setBusy(false);
292
+ }
293
+ };
294
+ return (_jsx(Box, { position: "fixed", inset: 0, z: "var(--z-modal)", display: "flex", items: "center", justify: "center", p: "$4", bg: "$sunken", onClick: (e) => {
295
+ if (e.target === e.currentTarget)
296
+ onClose();
297
+ }, children: _jsxs(YStack, { role: "dialog", "aria-label": "Invite", width: "100%", maxW: 440, gap: "$3", p: "$5", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { fontSize: "$5", fontWeight: "600", color: "$ink", children: "Invite" }), link ? (_jsxs(_Fragment, { children: [_jsxs(Text, { fontSize: "$2", color: "$soft", children: ["This link joins ", owner, ". It redeems once."] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { flex: 1, fontSize: "$2", color: "$ink", numberOfLines: 1, children: link }), _jsx(Take, { text: link, says: "the invitation link", label: "Copy" })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { render: _jsx("input", { value: email, onChange: (e) => setEmail(e.target.value), onKeyDown: (e) => {
298
+ if (e.key === 'Enter')
299
+ void make();
300
+ }, placeholder: "Their email \u2014 or leave it open to anyone with the link", "aria-label": "Email", autoFocus: true }), width: "100%", bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: "var(--radius-md)", px: 10, py: 8, outlineStyle: "none", color: "inherit", fontSize: "$2" }), wrong ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: wrong })) : null] })), _jsxs(XStack, { justify: "flex-end", gap: "$2", children: [_jsx(Control, { onPress: onClose, children: link ? 'Done' : 'Cancel' }), link ? null : (_jsx(Control, { onPress: () => void make(), strong: true, children: busy ? 'Making…' : 'Make a link' }))] })] }) }));
301
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The four ways this experience is customized, and the whole taxonomy.
3
+ *
4
+ * APPS are connected services — the thing that used to be called Connectors,
5
+ * which named the wire rather than what a reader installs. CHANNELS are
6
+ * transports: where a conversation reaches you. PLUGINS extend the client.
7
+ * SKILLS extend what an agent knows how to do.
8
+ *
9
+ * Order is the ruling's: Apps, Channels, Plugins, Skills.
10
+ */
11
+ export type Tab = 'apps' | 'channels' | 'plugins' | 'skills';
12
+ export declare function Directory({ open, onClose, tab: first, }: {
13
+ open: boolean;
14
+ onClose: () => void;
15
+ /** The tab the directory opens on. A door that leads to channels opens on them. */
16
+ tab?: Tab;
17
+ }): import("react").JSX.Element;
18
+ /**
19
+ * The body. Rendered inside the Directory modal AND inside Settings, which is
20
+ * why it is exported and takes the tab as a prop.
21
+ */
22
+ export declare function Catalog({ tab }: {
23
+ tab: Tab;
24
+ }): import("react").JSX.Element;