@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/Inbox.js ADDED
@@ -0,0 +1,193 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The inbox: what arrived, on whichever network carried it.
4
+ //
5
+ // IT READS LIKE MAIL, because that is what it is. The list stands beside what is
6
+ // open rather than being replaced by it: picking a message used to swap the
7
+ // whole pane, so choosing the next one meant going back first, and the list —
8
+ // the thing that tells you which to read — was never on screen while you read.
9
+ // Two columns is how every mail client has answered this, and the reason is the
10
+ // same here.
11
+ //
12
+ // ONE READING, MANY TRANSPORTS. `channel` is measured by the server, so the
13
+ // mark beside each face is a fact about the message. Nothing here enumerates
14
+ // the networks; `network.tsx` names the ones it can draw and falls back for the
15
+ // rest, so a fifth transport appears the day the platform reports one.
16
+ //
17
+ // IT HOLDS WHAT OTHER NETWORKS CARRY IN, and only that. The rooms this org
18
+ // talks in are places on this platform, read and answered on /home
19
+ // (`rooms.ts`); an arrival is a thread you REPLY to on the transport it came
20
+ // in on, and `conversations.ts` keys the two apart so one selection slot never
21
+ // holds two id spaces.
22
+ //
23
+ // IT IS TWO-WAY NOW, and the note that said otherwise had outlived the route it
24
+ // described. `@hanzo/ai`'s channels client still opens "INBOUND ONLY … a client
25
+ // that offered send() would be inventing a route", and the platform publishes
26
+ // `POST /v1/channels/{channel}/send` — measured on api.hanzo.ai, with the
27
+ // envelope's outbound projection spelled in its own description. So the reply
28
+ // goes through `client.http` here rather than through a client method that does
29
+ // not exist yet; when the SDK gains one, this call site is the only thing that
30
+ // moves.
31
+ //
32
+ // A reply box that cannot reply is still worse than none, which is why this one
33
+ // is drawn only where there is a room id to address.
34
+ import { useCallback, useMemo, useRef, useState } from 'react';
35
+ import { ChannelHeader } from '@hanzo/ui/agents';
36
+ import { Composer } from '@hanzo/ui/chat';
37
+ import { Box, Text, XStack, YStack } from '@hanzo/ui';
38
+ import { useIam } from '@hanzo/iam/react';
39
+ import { Plus } from 'lucide-react';
40
+ import { useAi } from './lib/ai.js';
41
+ import { useOpen } from './open.js';
42
+ import { Face } from './cast.js';
43
+ import { plainly } from './failure.js';
44
+ import { Network, of, roomName, speaker } from './network.js';
45
+ import { conversationOf, useConversations, } from './conversations.js';
46
+ import { Directory } from './Directory.js';
47
+ import { BAD } from './lib/mix.js';
48
+ /** Unix SECONDS, which is what the platform sends. Milliseconds here read as 1970. */
49
+ const when = (seconds) => new Date(seconds * 1000).toLocaleString(undefined, {
50
+ month: 'short',
51
+ day: 'numeric',
52
+ hour: 'numeric',
53
+ minute: '2-digit',
54
+ });
55
+ /** Today says the time; anything older says the day. A list scans on the part
56
+ * that differs, and for mail from this morning that is the clock. */
57
+ function stamp(seconds) {
58
+ const then = new Date(seconds * 1000);
59
+ const now = new Date();
60
+ const sameDay = then.getFullYear() === now.getFullYear() &&
61
+ then.getMonth() === now.getMonth() &&
62
+ then.getDate() === now.getDate();
63
+ return then.toLocaleString(undefined, sameDay ? { hour: 'numeric', minute: '2-digit' } : { month: 'short', day: 'numeric' });
64
+ }
65
+ /**
66
+ * WHO WROTE, AND FROM WHERE — one mark.
67
+ *
68
+ * The face is a disc and the network is a square tucked into its corner,
69
+ * because they answer two different questions and a reader should not have to
70
+ * tell two circles apart. The disc draws neither picture nor initials: the
71
+ * platform sends no likeness, and there is no name to reduce to letters — it
72
+ * drew the head of the platform's id, which on Slack is the same letter for
73
+ * everybody, so it told nobody apart while looking like somebody's initials.
74
+ * A disc with nothing to say says '?', which is what it means.
75
+ */
76
+ function Sender({ channel, size = 36 }) {
77
+ return (_jsxs(YStack, { width: size, height: size, shrink: 0, position: "relative", children: [_jsx(Face, { size: size }), _jsx(YStack, { position: "absolute", b: -2, r: -2, children: _jsx(Network, { channel: channel, size: Math.round(size * 0.46) }) })] }));
78
+ }
79
+ /** Where a network is connected. Opens My Channels modal. */
80
+ function Connect() {
81
+ const [modalOpen, setModalOpen] = useState(false);
82
+ return (_jsxs(_Fragment, { children: [_jsx(Box, { render: "button", onClick: () => setModalOpen(true), px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Plus, { size: 13, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "$ink", children: "Connect an app" })] }) }), _jsx(Directory, { open: modalOpen, onClose: () => setModalOpen(false), tab: "channels" })] }));
83
+ }
84
+ function Empty({ children, offer }) {
85
+ 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: "Inbox" }), _jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 420, children: children }), offer ? _jsx(Connect, {}) : null] }));
86
+ }
87
+ /** The list, which stays on screen while you read down it. */
88
+ function Arrivals({ rooms, open, onPick, }) {
89
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { height: 48, shrink: 0, items: "center", px: "$3", gap: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { flex: 1, fontSize: "$3", fontWeight: "600", color: "$ink", children: "Inbox" }), _jsx(Connect, {})] }), _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", children: rooms.map((one) => (_jsx(Box, { render: "button", onClick: () => onPick(one.key), "aria-current": one.key === open, display: "block", width: "100%", cursor: "pointer", borderBottomWidth: 1, borderColor: "var(--borderColor, var(--white-08))", bg: one.key === open ? 'var(--raised, var(--white-06))' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { gap: "$3", items: "center", p: "$3", children: [_jsx(Sender, { channel: one.channel }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", items: "flex-start", children: [_jsxs(XStack, { width: "100%", items: "baseline", gap: "$2", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, flex: 1, text: "left", children: one.title }), one.lastAt ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: stamp(one.lastAt / 1000) })) : null] }), one.preview ? (_jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: one.preview })) : null] })] }) }, one.key))) })] }));
90
+ }
91
+ /** What is open, beside the list rather than instead of it. */
92
+ function Reading({ room, reload, }) {
93
+ const { client } = useAi();
94
+ const [draft, setDraft] = useState('');
95
+ const [sending, setSending] = useState(false);
96
+ const [failed, setFailed] = useState(null);
97
+ /**
98
+ * Say it back, on the transport it arrived on.
99
+ *
100
+ * IDENTITY IS NOT A FIELD: the channel is the path segment and the sender is
101
+ * the caller's validated org, and the route refuses a body carrying `sender`,
102
+ * `account` or `channel` with a 400 rather than dropping it quietly. So the
103
+ * body is the room and the words, and nothing else.
104
+ *
105
+ * The room's own refusals are the reader's situation and are shown as the
106
+ * platform words them — a room this org has not bound is 403, and one whose
107
+ * route the bot has never learned is 409, which means somebody has to message
108
+ * it there first. `plainly` keeps those and answers for the estate's own.
109
+ *
110
+ * The list is re-read rather than appended to: what the transport accepted is
111
+ * what it assigned an id and a landing second to, and inventing a local row
112
+ * would put a message on screen that may not have gone.
113
+ */
114
+ const send = useCallback(async () => {
115
+ const text = draft.trim();
116
+ if (!client || !text || sending || !room.roomId)
117
+ return;
118
+ setSending(true);
119
+ setFailed(null);
120
+ try {
121
+ await client.http.json({
122
+ method: 'POST',
123
+ path: `/v1/channels/${encodeURIComponent(room.channel)}/send`,
124
+ body: { room: { id: room.roomId }, text },
125
+ });
126
+ setDraft('');
127
+ reload();
128
+ }
129
+ catch (e) {
130
+ setFailed(plainly(e, 'this room'));
131
+ }
132
+ finally {
133
+ setSending(false);
134
+ }
135
+ }, [client, draft, sending, room.channel, room.roomId, reload]);
136
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsx(ChannelHeader, { name: roomName(room), children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Network, { channel: room.channel, size: 14 }), _jsx(Text, { fontSize: "$2", color: "$soft", children: of(room.channel).name })] }) }), _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$3", children: [...room.messages]
137
+ .sort((a, b) => a.createdAt - b.createdAt)
138
+ .map((message) => (_jsxs(XStack, { gap: "$3", items: "flex-start", children: [_jsx(Sender, { channel: room.channel, size: 32 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsxs(XStack, { gap: "$2", items: "baseline", children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", color: "$ink", children: speaker(room.channel, message.sender) }), _jsx(Text, { fontSize: "$1", color: "$soft", children: when(message.createdAt) })] }), _jsx(Text, { fontSize: "$3", color: "$ink", children: message.text })] })] }, message.id))) }), _jsx(YStack, { p: "$3", gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: room.roomId ? (_jsxs(_Fragment, { children: [_jsx(Composer, { value: draft, onChange: setDraft, onSend: () => void send(), busy: sending, placeholder: `Reply on ${of(room.channel).name}`, label: `Reply on ${of(room.channel).name}` }), failed ? (_jsx(Text, { fontSize: "$2", color: BAD, children: failed })) : null] })) : (_jsx(Text, { fontSize: "$2", color: "$soft", children: "This message names no room to answer in." })) })] }));
139
+ }
140
+ function InboxGrip({ span, onSpan, }) {
141
+ const held = useRef(0);
142
+ return (_jsx(Box, { render: "div", role: "separator", "aria-orientation": "vertical", "aria-label": "Resize inbox channel list", "aria-valuenow": span, tabIndex: 0, position: "absolute", t: 0, b: 0, r: -3, width: 6, z: 10, cursor: "col-resize", hoverStyle: { bg: '$borderColor' }, onPointerDown: (e) => {
143
+ e.currentTarget.setPointerCapture(e.pointerId);
144
+ held.current = e.clientX - span;
145
+ }, onPointerMove: (e) => {
146
+ if (!e.currentTarget.hasPointerCapture(e.pointerId))
147
+ return;
148
+ const want = e.clientX - held.current;
149
+ const clamped = Math.min(540, Math.max(220, want));
150
+ onSpan(clamped);
151
+ }, onPointerUp: () => {
152
+ try {
153
+ localStorage.setItem('hanzo.inbox.span', String(span));
154
+ }
155
+ catch { }
156
+ } }));
157
+ }
158
+ function ReadingInbox() {
159
+ const { room, openRoom } = useOpen();
160
+ const { conversations, loading, reload } = useConversations();
161
+ const [listSpan, setListSpan] = useState(() => {
162
+ if (typeof window === 'undefined')
163
+ return 320;
164
+ try {
165
+ const stored = localStorage.getItem('hanzo.inbox.span');
166
+ return stored ? Math.max(220, Math.min(540, parseInt(stored, 10))) : 320;
167
+ }
168
+ catch {
169
+ return 320;
170
+ }
171
+ });
172
+ const open = useMemo(() => conversationOf(conversations, room), [conversations, room]);
173
+ if (loading && conversations.length === 0)
174
+ return _jsx(Empty, { children: "Reading your messages." });
175
+ if (conversations.length === 0) {
176
+ return (_jsx(Empty, { offer: true, children: "Nothing has arrived yet. Connect an app and the messages it carries land here \u2014 mail, chat, and the networks your work already happens on." }));
177
+ }
178
+ return (_jsxs(XStack, { flex: 1, minH: 0, position: "relative", children: [_jsxs(YStack, { display: open ? 'none' : 'flex', $md: { display: 'flex', width: listSpan, shrink: 0, borderRightWidth: 1 }, flex: 1, minH: 0, borderColor: "$borderColor", position: "relative", children: [_jsx(Arrivals, { rooms: conversations, open: room, onPick: openRoom }), _jsx(InboxGrip, { span: listSpan, onSpan: setListSpan })] }), open ? (_jsx(YStack, { flex: 1, minW: 0, minH: 0, children: _jsx(Reading, { room: open.arrival, reload: reload }) })) : (_jsx(YStack, { display: "none", $md: { display: 'flex' }, flex: 1, minW: 0, children: _jsx(Empty, { children: "Pick a conversation to read it." }) }))] }));
179
+ }
180
+ /**
181
+ * The pane, and the session its reads need.
182
+ *
183
+ * SPLIT so the hooks below never run for a reader with none: every route this
184
+ * pane reads is org-scoped and answers 403 without one, and a hook cannot be
185
+ * called conditionally — so the condition is a component boundary rather than
186
+ * an `if` the rules forbid.
187
+ */
188
+ export function Inbox() {
189
+ const { isLoading } = useIam();
190
+ if (isLoading)
191
+ return _jsx(Empty, { children: "\u00A0" });
192
+ return _jsx(ReadingInbox, {});
193
+ }
@@ -0,0 +1,3 @@
1
+ export declare function Market({ scope }: {
2
+ scope: 'all' | 'installed';
3
+ }): import("react").JSX.Element;
package/dist/Market.js ADDED
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The shelf: agents an org can bring into Hanzo, and the ones it already has.
4
+ //
5
+ // EVERY ROW IS REAL. The listings come from the platform's canonical copy of
6
+ // registry.modelcontextprotocol.io — tens of thousands of published servers —
7
+ // so this file names no vendor and hardcodes no catalogue. A brand appears here
8
+ // the day its publisher pushes an MCP server, and disappears when they pull it,
9
+ // without this surface being edited.
10
+ //
11
+ // INSTALL IS THE PLATFORM'S ONE INSTALL. `POST /v1/tools/mcp/servers` with a
12
+ // listing id creates the SAME MCPServer row that typing a URL by hand creates,
13
+ // so an org has one place its servers live however they got there.
14
+ //
15
+ // A STDIO-ONLY LISTING CANNOT BE INSTALLED HERE and the card says so rather
16
+ // than offering a button that fails: a remote endpoint is something this app can
17
+ // reach, a stdio package is a process someone still has to run.
18
+ import { useCallback, useEffect, useMemo, useState } from 'react';
19
+ import { say } from './failure.js';
20
+ import { Check, Search } from 'lucide-react';
21
+ import { Box, Text, XStack, YStack } from '@hanzo/ui';
22
+ import { useIam } from '@hanzo/iam/react';
23
+ import { hasSession } from './lib/session.js';
24
+ import { useAi } from './lib/ai.js';
25
+ /** A listing reachable over HTTP is one this app can connect on its own. */
26
+ const reachable = (listing) => (listing.transports ?? []).includes('streamable-http');
27
+ /** Labels a namespace carries that name no brand: `com.cloudflare.mcp` is
28
+ * Cloudflare, and `io.sanity.www` is Sanity. */
29
+ const PLUMBING = new Set(['mcp', 'www', 'api', 'app', 'cloud', 'io', 'server']);
30
+ const label = (listing) => {
31
+ if (listing.title)
32
+ return listing.title;
33
+ // A listing without a title still has a publisher, and a reverse-DNS vendor
34
+ // names it — but the LAST label is not always the brand: `com.cloudflare.mcp`
35
+ // ends in the word every entry here would end in. So read from the right and
36
+ // take the first label that is a name rather than plumbing.
37
+ const brand = (listing.vendor ?? '')
38
+ .split('.')
39
+ .reverse()
40
+ .find((part) => part && !PLUMBING.has(part));
41
+ if (brand)
42
+ return brand.charAt(0).toUpperCase() + brand.slice(1);
43
+ return listing.name?.split('/').pop() || listing.id;
44
+ };
45
+ function Empty({ children }) {
46
+ return (_jsx(YStack, { flex: 1, items: "center", justify: "center", p: "$6", children: _jsx(Text, { fontSize: "$3", color: "$soft", text: "center", children: children }) }));
47
+ }
48
+ /** The publisher's mark, or its initial. Never a generated face for a brand. */
49
+ function Mark({ listing }) {
50
+ return (_jsx(YStack, { width: 40, height: 40, shrink: 0, rounded: "$3", bg: "$panel", items: "center", justify: "center", overflow: "hidden", children: listing.logo ? (_jsx("img", { src: listing.logo, alt: "", width: 40, height: 40 })) : (_jsx(Text, { fontSize: "$4", fontWeight: "500", color: "$soft", children: label(listing).slice(0, 1).toUpperCase() })) }));
51
+ }
52
+ function Card({ listing, installed, onInstall, busy, }) {
53
+ const canInstall = reachable(listing);
54
+ return (_jsxs(YStack, { width: 300, gap: "$3", p: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$4", children: [_jsxs(XStack, { gap: "$3", items: "flex-start", children: [_jsx(Mark, { listing: listing }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", color: "$ink", numberOfLines: 1, children: label(listing) }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: listing.vendor ?? '' })] })] }), _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 3, children: listing.description || 'No description published.' }), installed ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Check, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Installed" })] })) : canInstall ? (_jsx(Box, { render: "button", onClick: onInstall, self: "flex-start", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$2", color: "$ink", children: busy ? 'Installing' : 'Install' }) })) : (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Ships as a ", (listing.packages ?? [])[0]?.registry ?? 'stdio', " package \u2014 needs a runner before it can be connected."] }))] }));
55
+ }
56
+ export function Market({ scope }) {
57
+ const { client } = useAi();
58
+ // SIGNED IN IS THE CREDENTIAL, NOT THE PROFILE. This gated on `user`, which is
59
+ // the answer to a userinfo REQUEST — and `IamProvider` swallows that request's
60
+ // failure, so a blip or a slow reply left `isAuthenticated` true and `user`
61
+ // null, and this pane told somebody holding a perfectly good token to sign in.
62
+ // The token is what the API accepts; the profile is decoration on top of it.
63
+ const { isAuthenticated, isLoading } = useIam();
64
+ const [ask, setAsk] = useState('');
65
+ const [listings, setListings] = useState(null);
66
+ const [mine, setMine] = useState([]);
67
+ const [total, setTotal] = useState(0);
68
+ const [busy, setBusy] = useState(null);
69
+ const [failed, setFailed] = useState(null);
70
+ // The shelf is org-scoped, so a signed-out reader has none to read. Asking
71
+ // anyway answers 403 and the SDK's sentence is written for whoever wired the
72
+ // client, not for a reader.
73
+ const ready = Boolean(client && isAuthenticated);
74
+ const reload = useCallback(() => {
75
+ if (!ready || !client)
76
+ return;
77
+ client.tools
78
+ .mcp()
79
+ .then(setMine)
80
+ .catch(() => setMine([]));
81
+ }, [client, ready]);
82
+ useEffect(reload, [reload]);
83
+ useEffect(() => {
84
+ if (!ready || !client)
85
+ return;
86
+ let live = true;
87
+ // The catalog is the one paginated surface here, so it is read whole and
88
+ // the page size is stated rather than left to the default.
89
+ client.tools
90
+ // BROWSING SHOWS THE SHELF; SEARCHING SHOWS THE WAREHOUSE.
91
+ //
92
+ // The registry holds tens of thousands of servers, and a grid of all of
93
+ // them is a directory listing rather than a storefront — the first screen
94
+ // would be whatever sorts first alphabetically. So an empty box asks for
95
+ // the curated ones and typing asks the whole catalog: same route, one
96
+ // parameter, and the count below says which shelf you are looking at.
97
+ .catalog({ q: ask || undefined, featured: ask ? undefined : true, limit: 60 })
98
+ .then((page) => {
99
+ if (!live)
100
+ return;
101
+ setListings(page.catalog ?? []);
102
+ setTotal(page.total ?? 0);
103
+ })
104
+ .catch((e) => live && setFailed(e.message));
105
+ return () => {
106
+ live = false;
107
+ };
108
+ }, [client, ready, ask]);
109
+ const installed = useMemo(() => new Set(mine.map((server) => server.listing).filter(Boolean)), [mine]);
110
+ const install = useCallback(async (listing) => {
111
+ if (!client)
112
+ return;
113
+ setBusy(listing.id);
114
+ try {
115
+ await client.http.json({
116
+ method: 'POST',
117
+ path: '/v1/tools/mcp/servers',
118
+ body: { listing: listing.id },
119
+ });
120
+ reload();
121
+ }
122
+ catch (e) {
123
+ setFailed(e.message);
124
+ }
125
+ finally {
126
+ setBusy(null);
127
+ }
128
+ }, [client, reload]);
129
+ const shown = useMemo(() => {
130
+ const all = listings ?? [];
131
+ return scope === 'installed' ? all.filter((l) => installed.has(l.id)) : all;
132
+ }, [listings, installed, scope]);
133
+ const signedIn = isAuthenticated || hasSession();
134
+ if (isLoading)
135
+ return _jsx(Empty, { children: "\u00A0" });
136
+ if (!signedIn)
137
+ return _jsx(Empty, { children: "Sign in to browse agents you can add." });
138
+ if (failed)
139
+ return _jsx(Empty, { children: say(failed, 'the marketplace') });
140
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { p: "$5", gap: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$7", lineHeight: "$7", fontWeight: "500", color: "$ink", children: "Put AI to work in Hanzo" }), _jsx(Text, { fontSize: "$3", color: "$soft", children: "Browse agents that bring your tools into Hanzo." }), _jsxs(XStack, { mt: "$2", width: 420, maxW: "100%", height: 36, items: "center", gap: "$2", px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 14, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: ask, onChange: (e) => setAsk(e.target.value), placeholder: "Search agents" }), flex: 1, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit" })] })] }), !listings ? (_jsx(Empty, { children: "Reading the shelf." })) : shown.length === 0 ? (_jsx(Empty, { children: scope === 'installed'
141
+ ? 'No agents installed yet.'
142
+ : ask
143
+ ? 'Nothing in the registry matches that.'
144
+ : 'The shelf is empty — the catalog has not been synced yet.' })) : (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", p: "$5", gap: "$3", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: scope === 'installed'
145
+ ? `${shown.length} installed`
146
+ : ask
147
+ ? `${shown.length} of ${total} matching`
148
+ : `${shown.length} featured` }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: shown.map((listing) => (_jsx(Card, { listing: listing, installed: installed.has(listing.id), busy: busy === listing.id, onInstall: () => void install(listing) }, listing.id))) })] }))] }));
149
+ }
package/dist/Me.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { type Host } from '@hanzo/build';
2
+ /** One organization as the menu draws it. */
3
+ export interface Org {
4
+ name: string;
5
+ display: string;
6
+ personal: boolean;
7
+ role: string;
8
+ }
9
+ /** The first letters of a name, at most two: Antje Worring → AW. */
10
+ export declare const initials: (name: string) => string;
11
+ /** The person's picture, or their initials. */
12
+ export declare function Face({ host, size }: {
13
+ host: Host;
14
+ size?: number;
15
+ }): import("react").JSX.Element;
16
+ export declare function Me({ host, narrow, onOrg, }: {
17
+ host: Host;
18
+ /** The collapsed rail's: the face alone, the menu opening beside it. */
19
+ narrow?: boolean;
20
+ /** Work in another organization: picked, then the app opened in it. */
21
+ onOrg: (org: string) => void;
22
+ }): import("react").JSX.Element;
23
+ /**
24
+ * What became of an organization this tab created: nothing to say once the token
25
+ * carries it (the app is already in it), and a plain line while it does not.
26
+ */
27
+ export declare function Made(): import("react").JSX.Element;
package/dist/Me.js ADDED
@@ -0,0 +1,251 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The account row at the foot of the app's column, and the menu it opens.
4
+ //
5
+ // The row says who is signed in and where: the person's face or initials, their
6
+ // name, and their role in the organization they work in. The menu lists every
7
+ // organization the token names — the personal one first, with its plan — and
8
+ // switches with `pick()` (lib/auth/session.ts), the one switch the app has.
9
+ // Under the list: Create organization, Organization settings, and Hanzo Team —
10
+ // opened in the same organization for a team, offered as "Create a team" for a
11
+ // personal one. Then the balance with Add funds, Settings, Usage, the plans,
12
+ // help and Log out. A SuperAdmin also gets All organizations (support mode).
13
+ //
14
+ // IAM is the source of every fact here: names and roles from the token's `orgs`
15
+ // claim, display names and which org is personal from `GET /v1/iam/organizations`,
16
+ // the plan and balance from billing. An organization is created by the same
17
+ // `POST /v1/account/orgs` the no-organization gate uses (components/workspace/
18
+ // Orgs.tsx); the next load mints a token that carries it.
19
+ import { useEffect, useRef, useState } from 'react';
20
+ import { Popover, SizableText, View, XStack, YStack } from '@hanzo/gui';
21
+ import { Building2, ChevronsUpDown, ExternalLink, Gauge, LifeBuoy, LogOut, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
22
+ import { path, useWho } from '@hanzo/build';
23
+ import { useOrganizations } from '@hanzo/iam/react';
24
+ import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
25
+ import { glass } from '@hanzo/ui/glass';
26
+ import { formatCents } from '@hanzo/usage';
27
+ import { api } from './lib/api.js';
28
+ import { createOrg, say } from './lib/org.js';
29
+ import { useTier } from './lib/tier.js';
30
+ import { list } from './lib/list.js';
31
+ import { org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
32
+ import { payPage, planName } from './lib/plans.js';
33
+ import { ENTRY } from './lib/host.js';
34
+ import { SupportPicker } from './Support.js';
35
+ import { BAD } from './lib/mix.js';
36
+ /** The documentation Get help opens. */
37
+ const DOCS = 'https://docs.hanzo.ai/docs/dev';
38
+ /** Where an organization's own settings live. */
39
+ const ORG_SETTINGS = '/settings/organization';
40
+ /** An organization this tab created, until the token carries it. */
41
+ const MADE = 'hanzo:org:made';
42
+ /** A role as a person reads it: `owner` → Owner. */
43
+ const said = (role) => (role ? role.charAt(0).toUpperCase() + role.slice(1) : '');
44
+ /** The first letters of a name, at most two: Antje Worring → AW. */
45
+ export const initials = (name) => name
46
+ .split(/[\s@._-]+/)
47
+ .filter(Boolean)
48
+ .slice(0, 2)
49
+ .map((w) => w.charAt(0).toUpperCase())
50
+ .join('') || '?';
51
+ /** The plan's name as billing sells it. */
52
+ const planOf = (tier) => (tier ? planName(tier.plan) || tier.tier?.displayName || tier.tier?.name || '' : '');
53
+ /**
54
+ * The token's organizations with IAM's rows beside them: display name, and which
55
+ * one is the person's own. The personal organization leads, then the token's order.
56
+ */
57
+ function useOrgs(current) {
58
+ const { roles } = useOrganizations();
59
+ const names = orgs();
60
+ const [rows, setRows] = useState([]);
61
+ const key = names.join(',');
62
+ useEffect(() => {
63
+ if (!key)
64
+ return;
65
+ const stop = new AbortController();
66
+ fetch(`${api()}/v1/iam/organizations?limit=100`, { headers: scope(), signal: stop.signal })
67
+ .then((r) => (r.ok ? r.json() : null))
68
+ .then((body) => {
69
+ if (!stop.signal.aborted)
70
+ setRows(list(body?.organizations));
71
+ })
72
+ .catch(() => { });
73
+ return () => stop.abort();
74
+ }, [key, current]);
75
+ const drawn = names.map((name) => {
76
+ const row = rows.find((r) => r?.name === name);
77
+ return { name, display: row?.displayName || name, personal: Boolean(row?.isPersonal), role: roles?.[name] ?? '' };
78
+ });
79
+ return [...drawn.filter((o) => o.personal), ...drawn.filter((o) => !o.personal)];
80
+ }
81
+ /** The plan of an organization other than the current one: billing, asked in its name. */
82
+ function usePlan(org, enabled) {
83
+ const [plan, setPlan] = useState('');
84
+ useEffect(() => {
85
+ if (!enabled || !org)
86
+ return;
87
+ const stop = new AbortController();
88
+ fetch(`${api()}/v1/billing/tier`, { headers: { ...scope(), 'X-Org-Id': org }, signal: stop.signal })
89
+ .then((r) => (r.ok ? r.json() : null))
90
+ .then((t) => {
91
+ if (!stop.signal.aborted)
92
+ setPlan(planOf(t));
93
+ })
94
+ .catch(() => { });
95
+ return () => stop.abort();
96
+ }, [org, enabled]);
97
+ return plan;
98
+ }
99
+ /** The person's picture, or their initials. */
100
+ export function Face({ host, size = 28 }) {
101
+ const p = host.person;
102
+ return (_jsx(XStack, { width: size, height: size, rounded: 9999, items: "center", justify: "center", bg: "$raised", overflow: "hidden", shrink: 0, borderWidth: 1, borderColor: "$borderColor", children: p?.avatar ? (_jsx("img", { src: p.avatar, alt: "", width: size, height: size })) : (_jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: initials(p?.name || p?.email || '') })) }));
103
+ }
104
+ /** One row of the menu: a real button, so Enter, Space and focus are the browser's. */
105
+ function Item({ icon, label, sub, checked, onPress, trail, }) {
106
+ const radio = checked !== undefined;
107
+ return (_jsxs(XStack, { render: "button", ...{ role: radio ? 'menuitemradio' : 'menuitem' }, "aria-checked": radio ? checked : undefined, "data-item": "", onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 36, py: "$1.5", rounded: "$4", bg: checked ? '$edge' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { bg: '$hover', outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", width: "100%", children: [icon ? (_jsx(XStack, { width: 20, justify: "center", shrink: 0, opacity: 0.8, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, text: "left", children: label }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: sub })) : null] }), trail] }));
108
+ }
109
+ const Rule = () => _jsx(XStack, { height: 1, bg: "$borderColor", my: "$1", mx: "$1" });
110
+ const Label = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pt: "$1.5", pb: "$1", children: children }));
111
+ /** An organization's mark in the list: its initials on a rounded square. */
112
+ const Mark = ({ org }) => (_jsx(XStack, { width: 20, height: 20, rounded: "$2", items: "center", justify: "center", bg: "$raised", shrink: 0, children: org.personal ? (_jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: initials(org.display) })) : (_jsx(Building2, { size: 12, "aria-hidden": true })) }));
113
+ /** Arrows, Home and End move between the menu's rows; Escape closes it. */
114
+ function rove(e, close) {
115
+ const rows = [...e.currentTarget.querySelectorAll('[data-item]')];
116
+ const at = rows.indexOf(document.activeElement);
117
+ const to = e.key === 'ArrowDown' ? (at + 1) % rows.length : e.key === 'ArrowUp' ? (at - 1 + rows.length) % rows.length : e.key === 'Home' ? 0 : e.key === 'End' ? rows.length - 1 : -2;
118
+ if (e.key === 'Escape') {
119
+ e.preventDefault();
120
+ close();
121
+ return;
122
+ }
123
+ if (to === -2 || !rows.length)
124
+ return;
125
+ e.preventDefault();
126
+ rows[to]?.focus();
127
+ }
128
+ export function Me({ host, narrow = false, onOrg, }) {
129
+ const menu = useWho();
130
+ const [creating, setCreating] = useState(false);
131
+ const [supporting, setSupporting] = useState(false);
132
+ const row = useRef(null);
133
+ const content = useRef(null);
134
+ const current = host.org;
135
+ const all = useOrgs(current);
136
+ const here = all.find((o) => o.name === current) ?? null;
137
+ const personal = all.find((o) => o.personal) ?? null;
138
+ const { tier } = useTier(menu.open, current);
139
+ const other = usePlan(personal && personal.name !== current ? personal.name : null, menu.open);
140
+ const planFor = (o) => (o.name === current ? planOf(tier) : o.personal ? other : '');
141
+ const who = host.person?.name || host.person?.email || '';
142
+ const where = [said(here?.role ?? ''), here?.display ?? current].filter(Boolean).join(' · ');
143
+ // The organization in use takes focus once the menu is drawn — after the
144
+ // popover has placed its own focus — so the keys reach the rows at once.
145
+ const land = (node) => {
146
+ content.current = node;
147
+ if (!node)
148
+ return;
149
+ requestAnimationFrame(() => requestAnimationFrame(() => (node.querySelector('[data-item][aria-checked="true"]') ?? node.querySelector('[data-item]'))?.focus()));
150
+ };
151
+ const close = () => {
152
+ menu.onOpenChange(false);
153
+ row.current?.focus();
154
+ };
155
+ const go = (p) => {
156
+ menu.onOpenChange(false);
157
+ host.go(p);
158
+ };
159
+ if (!host.person)
160
+ return null;
161
+ return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: menu.onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: "100%", children: _jsxs(XStack, { ref: (el) => {
162
+ row.current = el instanceof HTMLElement ? el : null;
163
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 30 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), where ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: where })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflow: "scroll", children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
164
+ menu.onOpenChange(false);
165
+ if (o.name !== current)
166
+ onOrg(o.name);
167
+ } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(ORG_SETTINGS)) }), here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "Add funds", sub: typeof tier?.balance?.effectiveAvailable === 'number' ? `Credits ${formatCents(tier.balance.effectiveAvailable)}` : 'Credits', onPress: () => (menu.onOpenChange(false), window.open(payPage(), '_blank', 'noopener')), trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
168
+ }
169
+ /**
170
+ * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
171
+ * the creator's own. The token in hand predates it, so it is renewed and the app
172
+ * reloads in the new organization; `Made` says so if the new token does not carry
173
+ * it yet.
174
+ */
175
+ function Create({ open, onOpenChange }) {
176
+ const [name, setName] = useState('');
177
+ const [busy, setBusy] = useState(false);
178
+ const [refused, setRefused] = useState('');
179
+ useEffect(() => {
180
+ if (open) {
181
+ setName('');
182
+ setRefused('');
183
+ }
184
+ }, [open]);
185
+ const submit = async () => {
186
+ const words = name.trim();
187
+ if (!words)
188
+ return setRefused('Give it a name.');
189
+ if (busy)
190
+ return;
191
+ setBusy(true);
192
+ setRefused('');
193
+ try {
194
+ const org = await createOrg(words);
195
+ try {
196
+ sessionStorage.setItem(MADE, JSON.stringify({ org, name: words, from: current() }));
197
+ }
198
+ catch {
199
+ /* the next load simply says nothing */
200
+ }
201
+ pick(org);
202
+ renew();
203
+ window.location.assign('/');
204
+ }
205
+ catch (e) {
206
+ setRefused(say(e));
207
+ setBusy(false);
208
+ }
209
+ };
210
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Create organization" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "You own it. Agents, credits and members belong to it, and you switch to it from this menu." }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "Organization name", placeholder: "Acme", maxLength: 80, onKeyDown: (e) => {
211
+ if (e.key === 'Enter')
212
+ void submit();
213
+ } }), refused ? (_jsx(SizableText, { size: "$1", color: BAD, role: "alert", children: refused })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", rounded: 999, disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] }) }));
214
+ }
215
+ /**
216
+ * What became of an organization this tab created: nothing to say once the token
217
+ * carries it (the app is already in it), and a plain line while it does not.
218
+ */
219
+ export function Made() {
220
+ const [made, setMade] = useState(null);
221
+ useEffect(() => {
222
+ try {
223
+ const kept = JSON.parse(sessionStorage.getItem(MADE) || 'null');
224
+ if (!kept?.org)
225
+ return;
226
+ if (orgs().includes(kept.org))
227
+ return sessionStorage.removeItem(MADE);
228
+ // Still out of the token: go on working where the person was.
229
+ if (kept.from && current() !== kept.from && orgs().includes(kept.from)) {
230
+ pick(kept.from);
231
+ return window.location.reload();
232
+ }
233
+ setMade({ org: kept.org, name: kept.name || kept.org });
234
+ }
235
+ catch {
236
+ /* nothing kept */
237
+ }
238
+ }, []);
239
+ if (!made)
240
+ return null;
241
+ const dismiss = () => {
242
+ try {
243
+ sessionStorage.removeItem(MADE);
244
+ }
245
+ catch {
246
+ /* gone with the tab */
247
+ }
248
+ setMade(null);
249
+ };
250
+ return (_jsxs(XStack, { "data-slot": "made", role: "status", items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$5", borderWidth: 1, borderColor: "$borderColor", bg: "$edge", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$ink", children: `${made.name} was created, and your sign-in does not include it yet, so you are still in this organization. It appears here once IAM adds you to it.` }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: dismiss, p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 13, "aria-hidden": true }) })] }));
251
+ }