@hanzo/rooms 0.1.82 → 0.1.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/App.js CHANGED
@@ -241,5 +241,5 @@ function Window({ bar }) {
241
241
  shut();
242
242
  setFinding(true);
243
243
  };
244
- return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: view, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT }, children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, at: where, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, go: move, onMenu: () => setDrawer(true), onChat: toChat, onDev: toDev }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, onSeek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }) })] })) : null, finding ? _jsx(Palette, { go: move, onClose: found }) : null] }) }) }) }));
244
+ return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: view, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT }, children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, onStudio: toStudio, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, go: move, onMenu: () => setDrawer(true), onChat: toChat, onDev: toDev }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, onSeek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, onOrg: org }) }) })] })) : null, finding ? _jsx(Palette, { go: move, onClose: found, mode: mode, host: host }) : null] }) }) }) }));
245
245
  }
package/dist/Chat.js CHANGED
@@ -42,6 +42,7 @@ import { AgentMaker, HOUSE, useAgentShelf, useCrew } from "./crew.js";
42
42
  import { called } from "./team.js";
43
43
  import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
44
44
  import { useMedia } from "@hanzo/gui";
45
+ import { tidy } from "./lib/title.js";
45
46
  import { useIam, useIamToken } from "@hanzo/iam/react";
46
47
  import { bearer, hasSession, org } from "./lib/session.js";
47
48
  import { api } from "./lib/api.js";
@@ -540,12 +541,13 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
540
541
  const at = channel(opened);
541
542
  // The first of them: what a surface that addresses one person reads.
542
543
  const withWhom = room[0] ?? null;
543
- // THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: open or
544
- // shut as Chat was left on a laptop, a sheet asked for on a phone, its tabs
545
- // kept per conversation. The header's Panel button and ⌘. move it.
544
+ // THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: shut
545
+ // until the reader opens it or a preview lands (`see`, a link that frames),
546
+ // kept per conversation with its tabs, so a chat opens as it was left and
547
+ // never as the last chat was. The header's Panel button and ⌘. move it.
546
548
  const media = useMedia();
547
549
  const wide = media.lg;
548
- const [panel, setPanel] = usePanel("chat", false, wide);
550
+ const [panel, setPanel] = usePanel(`chat:${at}`, false, wide);
549
551
  const deck = useDeck(`chat:${at}`, SEED);
550
552
  // The org's sites, which an empty tab suggests.
551
553
  const sites = useSites();
@@ -1464,7 +1466,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1464
1466
  }, children: [_jsx(Schedule, { open: plan !== null, onOpenChange: (open) => {
1465
1467
  if (!open)
1466
1468
  setPlan(null);
1467
- }, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: mine.title || "Untitled" })) : null, room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, onSchedule: anonymous ? undefined : () => setPlan(lastAsked()), children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { sections: sections, onOpen: () => setPanel(true) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
1469
+ }, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: tidy(mine.title) })) : null, room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, onSchedule: anonymous ? undefined : () => setPlan(lastAsked()), children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { sections: sections, onOpen: () => setPanel(true) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
1468
1470
  // AN EMPTY ROOM CENTRES ITS COMPOSER on a laptop. While nothing has been
1469
1471
  // said the opening (the heading) and the composer sit as one group in the
1470
1472
  // middle of the height; the moment a turn exists the transcript takes the
@@ -1531,7 +1533,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1531
1533
  empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", py: "$6", ...(media.sm ? null : { flex: 1, justify: "center" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { ml: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room) || heading || "What can I help with?" }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
1532
1534
  // The transcript reads at the same measure. `Thread` defaults to 768
1533
1535
  // itself; handing it the column here is what lets Width reach it.
1534
- column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1536
+ column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsxs(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: [_jsx(Anchor, { href: site("/legal/terms"), fontSize: "$1", color: "$soft", children: "Terms" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "\u00B7" }), _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$soft", children: "Privacy" })] }), consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1535
1537
  }
1536
1538
  /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1537
1539
  function Consent({ question, onAgree, onCancel, }) {
package/dist/Column.js CHANGED
@@ -21,6 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
21
  import { Text, XStack, YStack } from '@hanzo/gui';
22
22
  import { Bell, Search, Settings2, SquarePen } from 'lucide-react';
23
23
  import { DOTS, Grip, nav, NEW, path as written, useSessions } from '@hanzo/build';
24
+ import { tidy } from './lib/title.js';
24
25
  import { DropdownMenu } from '@hanzo/ui';
25
26
  import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
26
27
  import { useId } from 'react';
@@ -78,7 +79,7 @@ function Activity({ host }) {
78
79
  ...(latest.length
79
80
  ? latest.map((s) => ({
80
81
  key: s.id,
81
- label: s.title || 'Untitled run',
82
+ label: tidy(s.title, 56, 'Untitled run'),
82
83
  description: [s.status, ago(s.updatedAt || s.createdAt)].filter(Boolean).join(' · '),
83
84
  icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }),
84
85
  onSelect: () => host.go(s.id),
package/dist/Me.js CHANGED
@@ -20,7 +20,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
20
20
  // personal one first, with its plan — switched with `onOrg`; Create
21
21
  // organization, Organization settings, and Hanzo Team (opened in the same
22
22
  // organization for a team, offered as "Create a team" for a personal one); and
23
- // All organizations for a SuperAdmin (support mode).
23
+ // All organizations for a SuperAdmin, which opens the chooser (Orgs.tsx).
24
24
  //
25
25
  // IAM is the source of every fact here: names and roles from the token's `orgs`
26
26
  // claim, display names and which org is personal from `GET /v1/iam/organizations`,
@@ -36,13 +36,12 @@ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuC
36
36
  import { api } from './lib/api.js';
37
37
  import { createOrg, say } from './lib/org.js';
38
38
  import { list } from './lib/list.js';
39
- import { org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
39
+ import { choose, org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
40
40
  import { ENTRY } from './lib/host.js';
41
41
  import { leave } from './dev.js';
42
42
  import { asked, KEYS, legal, out, pages } from './help.js';
43
43
  import { showSettings } from './open.js';
44
44
  import { site, where } from './where.js';
45
- import { SupportPicker } from './Support.js';
46
45
  import { BAD } from './lib/mix.js';
47
46
  /** Where an organization's own settings live: a page of the site (`site()`), not of the app. */
48
47
  const ORG_SETTINGS = '/settings/organization';
@@ -138,7 +137,6 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
138
137
  const [account, setAccount] = useState(false);
139
138
  const [help, setHelp] = useState(false);
140
139
  const [creating, setCreating] = useState(false);
141
- const [supporting, setSupporting] = useState(false);
142
140
  const [keys, setKeys] = useState(false);
143
141
  const row = useRef(null);
144
142
  const current = host.org;
@@ -184,7 +182,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
184
182
  setHelp(false);
185
183
  }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
186
184
  row.current = el instanceof HTMLElement ? el : null;
187
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 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 ? 26 : 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 }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: plan || host.person.email || '' })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: () => setSupporting(true), children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
185
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 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 ? 26 : 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 }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: plan || host.person.email || '' })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
188
186
  }
189
187
  /** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
190
188
  function Shortcuts({ open, onOpenChange, apple }) {
package/dist/Orgs.js CHANGED
@@ -28,11 +28,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
28
28
  // Free is offered a team, and an open one is sent on (`forward`). Behind the
29
29
  // door, and only there, nothing opens without a live plan.
30
30
  import { dev } from './lib/host.js';
31
- import { useCallback, useEffect, useMemo, useState } from 'react';
31
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
32
32
  import { useIam } from '@hanzo/iam/react';
33
33
  import { Text, View, XStack, YStack } from '@hanzo/gui';
34
- import { Button } from '@hanzo/ui';
35
- import { Minus, Plus, X } from 'lucide-react';
34
+ import { Avatar, Button, Tag } from '@hanzo/ui';
35
+ import { SearchInput } from '@hanzo/ui/product/Filters';
36
+ import { label, said as spoken } from '@hanzo/build/plan';
37
+ import { Check, Minus, Plus, X } from 'lucide-react';
36
38
  import { Waiting } from './waiting.js';
37
39
  import { LOGIN, enter } from './lib/destination.js';
38
40
  import { useAi } from './lib/ai.js';
@@ -40,7 +42,8 @@ import { useHost } from './lib/hostname.js';
40
42
  import { useHydrated } from './lib/hydrated.js';
41
43
  import { api } from './lib/api.js';
42
44
  import { usePersonal, useTier } from './lib/tier.js';
43
- import { ORG, PICKED, assumed, hasSession, org, orgs as claimed, pick, renew, scope } from './lib/session.js';
45
+ import { CHOOSE, ORG, PICKED, assumed, bearer, hasSession, org, orgs as claimed, pick, renew, role, scope, superAdmin, support } from './lib/session.js';
46
+ import { matches, refusal, slugify } from './lib/orgname.js';
44
47
  import { team as workspace } from './lib/host.js';
45
48
  import { checkoutUrl, money } from './lib/pay.js';
46
49
  import { cartUrl, charge, priced, usePlan, usePlans } from './lib/plans.js';
@@ -119,9 +122,17 @@ export function Orgs({ children, front = false, forward, signup = false, }) {
119
122
  window.removeEventListener('focus', back);
120
123
  };
121
124
  }, [reload]);
125
+ // The chooser over the room, asked for from the account menu (session.ts `choose`).
126
+ const [choosing, setChoosing] = useState(false);
127
+ useEffect(() => {
128
+ const ask = () => setChoosing(true);
129
+ window.addEventListener(CHOOSE, ask);
130
+ return () => window.removeEventListener(CHOOSE, ask);
131
+ }, []);
122
132
  const choose = useCallback((o) => {
123
133
  pick(o);
124
134
  setPicked(true);
135
+ setChoosing(false);
125
136
  }, []);
126
137
  // The organization a link arrives for. hanzo.ai opens the workspace at
127
138
  // `?signin&org=<the org it is working in>` (Me.tsx); once signed in, an org
@@ -198,7 +209,10 @@ export function Orgs({ children, front = false, forward, signup = false, }) {
198
209
  return door ? _jsx(Start, { onFounded: setFounding, first: true }) : _jsx(Create, {});
199
210
  }
200
211
  if (!current || (members.length > 1 && !picked))
201
- return _jsx(Pick, { members: members, onPick: choose });
212
+ return _jsx(Pick, { members: members, current: current, onPick: choose });
213
+ // Asked for over an open room, the room keeps its place under the chooser and closing it goes back.
214
+ if (choosing)
215
+ return _jsxs(_Fragment, { children: [_jsx(Held, { veiled: true, children: children }), _jsx(Pick, { members: members, current: current, onPick: choose, onClose: () => setChoosing(false) })] });
202
216
  if (open)
203
217
  return forward ? _jsx(Forward, { to: forward }) : _jsx(_Fragment, { children: _jsx(Held, { veiled: false, children: children }) });
204
218
  // IN THE WORKSPACE NOTHING OPENS WITHOUT A LIVE PLAN. Every branch here
@@ -458,9 +472,253 @@ function CreateDialog({ onClose }) {
458
472
  ? `A team organization runs on ${plan.name} at ${price} per seat a month, at least ${Number(plan.limits?.minSeats ?? 1)} seats.`
459
473
  : `A personal organization runs on ${plan.name} at ${price} a month.`, ' ', "Nothing in it works until that plan is live, so creating one takes you straight to checkout, where you choose the seats and the term.", ' ', "Each plan includes session and daily usage limits; usage never draws your credit balance.", ' ', "Payment is on the provider's hosted page."] })) : null, refused ? (_jsx(Text, { render: "p", role: "alert", mt: 14, fontSize: "$3", color: "var(--foreground)", children: refused })) : null, _jsxs(XStack, { justify: "flex-end", gap: 10, mt: 24, flexWrap: "wrap", children: [_jsx(Button, { type: "button", size: "lg", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", size: "lg", shrink: 1, py: "$2", disabled: !plan || busy, children: _jsx(Text, { fontFamily: "$body", fontSize: "$3", color: "inherit", text: "center", children: busy ? 'Creating…' : paid ? `Create and subscribe — ${price}${seat} / month` : 'Create' }) })] }), _jsx(Rule, { mt: 18, mb: 8 }), _jsx(Text, { render: "p", text: "right", fontSize: "$2", color: muted, fontFamily: "$mono", children: "Esc to close" })] }) }));
460
474
  }
461
- /** Several organizations: which one, this time. */
462
- function Pick({ members, onPick }) {
463
- return (_jsx(Page, { label: "Choose an organization", children: _jsxs(Column, { children: [_jsx(Brand, {}), _jsx(Heading, { title: "Choose an organization", lede: _jsx(_Fragment, { children: "Everything you open \u2014 chats, agents, credit \u2014 is this one's." }) }), _jsx(Rule, {}), _jsx(View, { display: "grid", gap: 10, children: members.map((o) => (_jsx(View, { render: "button", onClick: () => onPick(o), items: "flex-start", px: 16, py: 14, rounded: 12, borderWidth: 1, borderColor: "var(--border)", bg: "transparent", cursor: "pointer", children: _jsx(Text, { fontSize: "$4", color: "var(--foreground)", text: "left", children: o }) }, o))) })] }) }));
475
+ /**
476
+ * A page of IAM's organization list (`GET /v1/iam/organizations`): the reader's
477
+ * own organizations whole on the first page, then, for a SuperAdmin only, every
478
+ * other one, newest first. `q` narrows it by name or display name on the server;
479
+ * `cursor` continues it. IAM decides the scope from the bearer, so this asks the
480
+ * same thing for everyone.
481
+ */
482
+ async function listing(q, cursor, limit, signal) {
483
+ const query = new URLSearchParams({ limit: String(limit) });
484
+ if (q.trim())
485
+ query.set('q', q.trim());
486
+ if (cursor)
487
+ query.set('cursor', cursor);
488
+ const res = await fetch(`${api()}/v1/iam/organizations?${query}`, {
489
+ headers: { Authorization: `Bearer ${bearer() ?? ''}` },
490
+ signal,
491
+ });
492
+ if (!res.ok)
493
+ throw new Error(`IAM answered ${res.status}.`);
494
+ return (await res.json());
495
+ }
496
+ /** How each of the reader's organizations appears, by name, from IAM's rows: the first page carries all of them. */
497
+ function useMarks(members) {
498
+ const [rows, setRows] = useState({});
499
+ const key = members.join(',');
500
+ useEffect(() => {
501
+ if (!key)
502
+ return;
503
+ const stop = new AbortController();
504
+ listing('', '', 1, stop.signal)
505
+ .then((page) => {
506
+ if (!stop.signal.aborted)
507
+ setRows(Object.fromEntries((page.organizations ?? []).map((o) => [o.name, o])));
508
+ })
509
+ .catch(() => { });
510
+ return () => stop.abort();
511
+ }, [key]);
512
+ return rows;
513
+ }
514
+ /** Each organization's plan by its family (`Max 20x`), billing asked in its name; absent where it names none. */
515
+ function useBilled(members) {
516
+ const [plans, setPlans] = useState({});
517
+ const key = members.join(',');
518
+ useEffect(() => {
519
+ const stop = new AbortController();
520
+ for (const o of key.split(',').filter(Boolean)) {
521
+ fetch(`${api()}/v1/billing/tier`, { headers: { ...scope(), 'X-Org-Id': o }, signal: stop.signal })
522
+ .then((r) => (r.ok ? r.json() : null))
523
+ .then((t) => {
524
+ const plan = t ? spoken(label(t.plan, t.tier?.name)) : '';
525
+ if (plan && !stop.signal.aborted)
526
+ setPlans((had) => ({ ...had, [o]: plan }));
527
+ })
528
+ .catch(() => { });
529
+ }
530
+ return () => stop.abort();
531
+ }, [key]);
532
+ return plans;
533
+ }
534
+ /** A role as a person reads it: `owner` → Owner. */
535
+ const said = (role) => (role ? role.charAt(0).toUpperCase() + role.slice(1) : '');
536
+ /** An organization's mark: its image, else its emoji, else its initial in its own hue (keyed on the slug, so a rename keeps it). */
537
+ function Mark({ org }) {
538
+ const emoji = org.mark?.avatar ? '' : org.mark?.emoji;
539
+ return (_jsx(Avatar, { size: "xl", shape: "rounded", src: org.mark?.avatar || (emoji ? null : org.mark?.logo) || null, name: org.display, colorSeed: org.name, icon: emoji ? _jsx(Text, { fontSize: "$7", children: emoji }) : undefined, alt: "" }));
540
+ }
541
+ /** One organization: its mark, its name and slug, the reader's role and its plan; the current one checked. */
542
+ function Choice({ org, current = false, note, disabled, onPress }) {
543
+ return (_jsx(View, { render: "li", display: "block", children: _jsxs(XStack, { render: "button", onClick: onPress, disabled: disabled, "aria-current": current || undefined, width: "100%", items: "center", gap: 12, px: 12, py: 10, rounded: 12, borderWidth: 1, borderColor: current ? '$ink' : '$edge', bg: "transparent", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: [_jsx(Mark, { org: org }), _jsxs(YStack, { flex: 1, minW: 0, gap: 4, children: [_jsx(Text, { fontSize: "$4", fontWeight: "500", color: "$ink", numberOfLines: 1, text: "left", children: org.display }), _jsxs(XStack, { items: "center", gap: 6, flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", fontFamily: "$mono", color: "$soft", numberOfLines: 1, children: org.name }), org.role ? _jsx(Tag, { variant: "outline", children: said(org.role) }) : null, org.plan ? _jsx(Tag, { children: org.plan }) : null, note ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: note })) : null] })] }), current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null] }) }));
544
+ }
545
+ /**
546
+ * WHICH ORGANIZATION, THIS TIME — and, for a SuperAdmin, any organization.
547
+ *
548
+ * The reader's organizations are the token's `orgs` (no other list is theirs),
549
+ * drawn with IAM's rows beside them: the mark, the display name, the role the
550
+ * token signs and the plan billing reads in each one's name. The search filters
551
+ * them as it is typed, by slug or display name. A person founds another here
552
+ * (`Found`); a SuperAdmin, whom IAM never lets own a customer organization, is
553
+ * shown every organization instead (`Everyone`), stepped into through IAM.
554
+ */
555
+ function Pick({ members, current, onPick, onClose, }) {
556
+ // IAM's verdict as it signed it into the token (authz `Claims.Sudo`). It only
557
+ // decides what is drawn: IAM answers anyone else's list with their own
558
+ // organizations and refuses them the step in.
559
+ const sudo = superAdmin();
560
+ const [q, setQ] = useState('');
561
+ const [founding, setFounding] = useState(false);
562
+ const marks = useMarks(members);
563
+ const plans = useBilled(members);
564
+ const mine = members.map((name) => ({
565
+ name,
566
+ display: marks[name]?.displayName || name,
567
+ mark: marks[name],
568
+ role: role(name) ?? '',
569
+ plan: plans[name],
570
+ }));
571
+ const shown = mine.filter((o) => matches(o, q));
572
+ useEffect(() => {
573
+ if (!onClose || founding)
574
+ return;
575
+ const key = (e) => {
576
+ if (e.key === 'Escape')
577
+ onClose();
578
+ };
579
+ document.addEventListener('keydown', key);
580
+ return () => document.removeEventListener('keydown', key);
581
+ }, [onClose, founding]);
582
+ return (_jsxs(Page, { label: "Choose an organization", children: [_jsxs(Column, { children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsx(Brand, {}), onClose ? (_jsx(Button, { variant: "ghost", size: "icon-sm", mt: -6, mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })) : null] }), _jsx(Heading, { title: "Choose an organization", lede: _jsx(_Fragment, { children: "Everything you open \u2014 chats, agents, credit \u2014 is this one's." }) }), _jsx(Rule, {}), _jsx(XStack, { render: "form", onSubmit: (e) => {
583
+ e.preventDefault();
584
+ if (shown[0])
585
+ onPick(shown[0].name);
586
+ }, children: _jsx(SearchInput, { name: "organizations", value: q, onChange: setQ, placeholder: sudo ? 'Search every organization' : 'Find an organization', actions: sudo ? null : (_jsxs(Button, { type: "button", onClick: () => setFounding(true), children: [_jsx(Plus, { size: 16, "aria-hidden": true }), "New organization"] })) }) }), _jsx(YStack, { render: "ul", "aria-label": "Your organizations", m: 0, p: 0, mt: 14, gap: 8, children: shown.map((o) => (_jsx(Choice, { org: o, current: o.name === current, onPress: () => onPick(o.name) }, o.name))) }), shown.length === 0 ? (_jsxs(Text, { render: "p", mt: 14, fontSize: "$3", color: "$soft", children: ["None of yours matches \u201C", q.trim(), "\u201D."] })) : null, sudo ? _jsx(Everyone, { q: q, skip: members }) : null] }), founding ? _jsx(Found, { onClose: () => setFounding(false), onMade: onPick }) : null] }));
587
+ }
588
+ /**
589
+ * EVERY ORGANIZATION, for a SuperAdmin to step into.
590
+ *
591
+ * The list and the search are IAM's, a page at a time, and IAM records each new
592
+ * search under the operator's name. Opening one is IAM's `assume` (session.ts
593
+ * `support`): the operator's own token re-scoped to it and the step recorded in
594
+ * that organization's audit log. Nobody is made a member of anything.
595
+ */
596
+ function Everyone({ q, skip }) {
597
+ const [rows, setRows] = useState([]);
598
+ const [cursor, setCursor] = useState('');
599
+ const [loading, setLoading] = useState(false);
600
+ const [refused, setRefused] = useState(null);
601
+ const [stepping, setStepping] = useState(null);
602
+ // Only the newest read lands: a search typed while a page is in flight
603
+ // supersedes it, so a slow answer to an old query never overwrites the new one.
604
+ const asked = useRef(0);
605
+ const load = useCallback(async (query, from) => {
606
+ const mine = ++asked.current;
607
+ setLoading(true);
608
+ setRefused(null);
609
+ try {
610
+ const next = await listing(query, from, 20);
611
+ if (mine !== asked.current)
612
+ return;
613
+ setRows((had) => {
614
+ // IAM's cursor is a position, so an org created mid-walk can shift a row
615
+ // across a page edge; each org is kept once.
616
+ const all = from ? [...had, ...(next.organizations ?? [])] : next.organizations ?? [];
617
+ return all.filter((o, i) => all.findIndex((x) => x.name === o.name) === i);
618
+ });
619
+ setCursor(next.cursor ?? '');
620
+ }
621
+ catch (e) {
622
+ if (mine === asked.current)
623
+ setRefused(e instanceof Error ? e.message : 'The list did not load.');
624
+ }
625
+ finally {
626
+ if (mine === asked.current)
627
+ setLoading(false);
628
+ }
629
+ }, []);
630
+ useEffect(() => {
631
+ const id = setTimeout(() => void load(q, ''), 250);
632
+ return () => clearTimeout(id);
633
+ }, [q, load]);
634
+ const enter = (org) => {
635
+ setStepping(org);
636
+ setRefused(null);
637
+ support(org).catch((e) => {
638
+ setRefused(e instanceof Error ? e.message : 'IAM did not step in.');
639
+ setStepping(null);
640
+ });
641
+ };
642
+ const others = rows.filter((o) => !skip.includes(o.name));
643
+ return (_jsxs(YStack, { render: "section", "aria-label": "All organizations", mt: 36, children: [_jsx(Text, { render: "h2", fontSize: "$6", fontWeight: "500", color: "$ink", children: "All organizations" }), _jsx(Text, { render: "p", mt: 4, fontSize: "$3", color: "$soft", lineHeight: 21, children: "Support mode. Step into any organization to see it as it does; IAM records every step under your name." }), _jsx(YStack, { render: "ul", "aria-label": "Every organization", m: 0, p: 0, mt: 14, gap: 8, children: others.map((o) => (_jsx(Choice, { org: { name: o.name, display: o.displayName || o.name, mark: o }, note: stepping === o.name ? 'Stepping in…' : undefined, disabled: stepping !== null, onPress: () => enter(o.name) }, o.name))) }), !loading && others.length === 0 && !refused ? (_jsx(Text, { render: "p", mt: 8, fontSize: "$3", color: "$soft", children: "No organization matches." })) : null, refused ? (_jsx(Text, { render: "p", role: "alert", mt: 8, fontSize: "$3", color: BAD, children: refused })) : null, cursor ? (_jsx(Button, { self: "flex-start", mt: 10, onClick: () => void load(q, cursor), disabled: loading, children: loading ? 'Loading…' : 'More' })) : null] }));
644
+ }
645
+ /**
646
+ * FOUND AN ORGANIZATION: its name, then the slug IAM keys it by.
647
+ *
648
+ * The slug follows the name the way IAM's onboarding makes it, until it is typed
649
+ * over; a slug typed is IAM's to take as it is, so one IAM would not keep is
650
+ * refused here with the rule, never rewritten. IAM's own onboarding makes it
651
+ * (`POST /v1/iam/onboard`): the organization, the reader as its owner and its
652
+ * first API key, the founder's account untouched. It is sent the name when the
653
+ * name makes the slug, and the slug when the slug was typed, the display name
654
+ * then written on the row (`/v1/iam/organizations/profile`). The token in hand
655
+ * predates it, so it is refreshed before the organization opens.
656
+ */
657
+ function Found({ onClose, onMade }) {
658
+ const { sdk } = useIam();
659
+ const [name, setName] = useState('');
660
+ const [typed, setTyped] = useState(null);
661
+ const slug = typed ?? slugify(name);
662
+ const wrong = slug ? refusal(slug) : null;
663
+ const [busy, setBusy] = useState(false);
664
+ const [refused, setRefused] = useState(null);
665
+ useEffect(() => {
666
+ const key = (e) => {
667
+ if (e.key === 'Escape')
668
+ onClose();
669
+ };
670
+ document.addEventListener('keydown', key);
671
+ return () => document.removeEventListener('keydown', key);
672
+ }, [onClose]);
673
+ const submit = async (e) => {
674
+ e.preventDefault();
675
+ const display = name.trim();
676
+ if (busy)
677
+ return;
678
+ if (!display)
679
+ return setRefused('Give it a name.');
680
+ if (wrong)
681
+ return setRefused(wrong);
682
+ setBusy(true);
683
+ setRefused(null);
684
+ const headers = { Authorization: `Bearer ${bearer() ?? ''}`, 'Content-Type': 'application/json' };
685
+ const why = async (r) => {
686
+ const body = (await r.json().catch(() => null));
687
+ return body?.error || body?.detail || body?.msg || body?.message || `IAM answered ${r.status}.`;
688
+ };
689
+ try {
690
+ const named = slugify(display) === slug;
691
+ const r = await fetch(`${api()}/v1/iam/onboard`, { method: 'POST', headers, body: JSON.stringify({ name: named ? display : slug }) });
692
+ if (!r.ok)
693
+ throw new Error(await why(r));
694
+ const made = (await r.json().catch(() => null))?.org;
695
+ if (!made)
696
+ throw new Error('IAM made no organization. Try again.');
697
+ renew();
698
+ await sdk.refreshAccessToken().catch(() => null);
699
+ if (!named) {
700
+ const p = await fetch(`${api()}/v1/iam/organizations/profile`, {
701
+ method: 'POST',
702
+ headers: { ...headers, Authorization: `Bearer ${bearer() ?? ''}` },
703
+ body: JSON.stringify({ owner: 'admin', name: made, displayName: display }),
704
+ });
705
+ if (!p.ok)
706
+ throw new Error(`${made} is made, and it is still called ${made}: ${await why(p)}`);
707
+ }
708
+ // A token that does not carry it yet is minted on the next load, which opens it.
709
+ if (!claimed().includes(made)) {
710
+ pick(made);
711
+ window.location.reload();
712
+ return;
713
+ }
714
+ onMade(made);
715
+ }
716
+ catch (err) {
717
+ setRefused(err instanceof Error ? err.message : 'Could not create it.');
718
+ setBusy(false);
719
+ }
720
+ };
721
+ return (_jsx(YStack, { role: "dialog", "aria-modal": true, "aria-label": "New organization", position: "fixed", inset: 0, z: DIALOG, bg: shade(28), overflowY: "auto", py: "6vh", px: 16, onClick: onClose, children: _jsxs(YStack, { render: "form", onSubmit: submit, onClick: (e) => e.stopPropagation(), width: "100%", maxW: 480, mx: "auto", bg: "var(--background)", rounded: 16, borderWidth: 1, borderColor: "$edge", boxShadow: `0 24px 64px ${shade(28)}`, pt: 24, px: 24, pb: 20, children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsx(Text, { render: "h2", fontSize: "$8", fontWeight: "500", color: "$ink", children: "New organization" }), _jsx(Text, { render: "p", mt: 6, fontSize: "$3", color: "$soft", lineHeight: 21, children: "You own it. Its agents, credit and members are its own." })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(View, { render: "label", display: "block", mt: 20, children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "$ink", children: "Name" }), _jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), placeholder: "Acme", maxLength: 100, autoFocus: true }), ...FIELD })] }), _jsxs(View, { render: "label", display: "block", mt: 16, children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "$ink", children: "Slug" }), _jsx(Text, { render: _jsx("input", { value: slug, onChange: (e) => setTyped(e.target.value), placeholder: "acme", spellCheck: false, autoCapitalize: "off", autoCorrect: "off", "aria-invalid": Boolean(slug && wrong) || undefined }), ...FIELD, fontFamily: "$mono" }), _jsx(Text, { display: "block", fontSize: "$3", color: slug && wrong ? BAD : '$soft', mt: 8, children: slug && wrong ? wrong : 'How every request names it: lowercase letters, digits and hyphens. It does not change.' })] }), refused ? (_jsx(Text, { render: "p", role: "alert", mt: 14, fontSize: "$3", color: BAD, children: refused })) : null, _jsxs(XStack, { justify: "flex-end", gap: 10, mt: 22, flexWrap: "wrap", children: [_jsx(Button, { type: "button", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: busy || !name.trim() || Boolean(wrong), children: busy ? 'Creating…' : 'Create and open' })] })] }) }));
464
722
  }
465
723
  /** On the workspace's host, an organization whose plan is not live: it is offered the per-seat Team plan. */
466
724
  function Subscribe({ org, members, onSwitch, }) {
package/dist/Palette.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
+ import { type Host } from '@hanzo/build';
2
3
  import { type ShortcutKeys } from '@hanzo/ui';
4
+ import { type Place } from './lib/host.js';
3
5
  import { type Hit } from './seek.js';
4
6
  /** A row: what it is called, its glyph, and what pressing it does. */
5
7
  export interface Action extends Hit {
@@ -9,10 +11,14 @@ export interface Action extends Hit {
9
11
  /** Pressing it keeps the palette open. */
10
12
  stay?: boolean;
11
13
  }
12
- export declare function Palette({ onClose, go, actions, }: {
14
+ export declare function Palette({ onClose, go, actions, mode, host, }: {
13
15
  onClose: () => void;
14
16
  /** Moves the app to one of its addresses (lib/host `talk`, `app`). */
15
17
  go: (address: string) => void;
16
18
  /** The frame's own actions, after the palette's; one with a palette action's id stands in for it. */
17
19
  actions?: Action[];
20
+ /** The product the reader is in: what the empty field lists and what a query ranks first. */
21
+ mode?: Place['mode'];
22
+ /** Dev's host, whose runs the palette lists in Dev. */
23
+ host?: Host;
18
24
  }): import("react").JSX.Element;