@hanzo/rooms 0.1.58 → 0.1.61

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/Chat.js CHANGED
@@ -45,6 +45,7 @@ import { useMedia } from "@hanzo/gui";
45
45
  import { useIam, useIamToken } from "@hanzo/iam/react";
46
46
  import { bearer, hasSession, org } from "./lib/session.js";
47
47
  import { api } from "./lib/api.js";
48
+ import { dev } from "./lib/host.js";
48
49
  import { chats, house, paidPlan, paused, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
49
50
  import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
50
51
  import { WEB } from "./lib/web.js";
@@ -56,10 +57,10 @@ import { FileText, Image as ImageMark, Paperclip, X } from "lucide-react";
56
57
  import { FREE } from "./lib/ai.js";
57
58
  import { openThread, useOpen } from "./open.js";
58
59
  import { Beside, Framed } from "./Shell.js";
59
- import { SEED, Summary, look } from "./RightPane.js";
60
+ import { SEED, Summary, look, useSites } from "./RightPane.js";
60
61
  import { Share } from "./Share.js";
61
62
  import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
62
- import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, useDeck, useKey, useMind, usePanel, } from "@hanzo/build";
63
+ import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, useDeck, useKey, useMind, usePanel, frames, name as tabName, } from "@hanzo/build";
63
64
  import { checkoutUrl } from './lib/pay.js';
64
65
  import { Upgrade, useFree } from './Upgrade.js';
65
66
  import { enter, LOGIN } from './lib/destination.js';
@@ -238,7 +239,9 @@ export function Ask({ placeholder, idle, }) {
238
239
  * character (`Message ${persona.name}`). `label` stays "Ask anything", so the
239
240
  * accessible name does not move when the placeholder does.
240
241
  */
241
- const message = () => `Message ${brand()}`;
242
+ const message = () => `Ask ${brand()}`;
243
+ /** The line every answer is sent under, above the composer, as the reference sets it. */
244
+ const caution = () => `${brand()} can make mistakes. Check important info.`;
242
245
  /** The panel offers no kinds of its own in Chat: its tabs are pages. */
243
246
  const NONE = [];
244
247
  /** The id the room's system turn is held under in the thread. */
@@ -504,9 +507,12 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
504
507
  // THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: open or
505
508
  // shut as Chat was left on a laptop, a sheet asked for on a phone, its tabs
506
509
  // kept per conversation. The header's Panel button and ⌘. move it.
507
- const wide = useMedia().lg;
510
+ const media = useMedia();
511
+ const wide = media.lg;
508
512
  const [panel, setPanel] = usePanel("chat", false, wide);
509
513
  const deck = useDeck(`chat:${at}`, SEED);
514
+ // The org's sites, which an empty tab suggests.
515
+ const sites = useSites();
510
516
  useKey(() => setPanel(!panel));
511
517
  // On a laptop the frame's column is the panel's; a close from the frame (its
512
518
  // edge pulled shut, the shell's own control) comes back into `panel`.
@@ -1210,7 +1216,34 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1210
1216
  }, onCancel: () => {
1211
1217
  setAsking(null);
1212
1218
  } }));
1219
+ /** The tabs' browser, as this conversation offers it: the org's sites to start from, and a page asked about, captured or opened in Dev. */
1220
+ const browsing = {
1221
+ suggested: sites,
1222
+ onAsk: (page) => {
1223
+ const quote = page.selection ? `\n> ${page.selection.trim().split("\n").join("\n> ")}` : "";
1224
+ setDraft((was) => `${was ? `${was}\n\n` : ""}About ${page.title} (${page.url}):${quote}\n`);
1225
+ },
1226
+ onCapture: (file) => void hold(at, [file], via),
1227
+ onDev: (page) => {
1228
+ const slug = new URL(page.url).hostname.match(/^([a-z0-9-]+)\.hanzo\.app$/)?.[1] ?? null;
1229
+ window.location.assign(dev(slug));
1230
+ },
1231
+ };
1213
1232
  return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
1233
+ // A LINK IN AN ANSWER OPENS IN A TAB beside the conversation when the tab
1234
+ // can show it — a site or preview we host — and in a browser tab of its
1235
+ // own otherwise, as a modified click always does.
1236
+ onClickCapture: (press) => {
1237
+ const e = press;
1238
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
1239
+ return;
1240
+ const a = e.target?.closest?.("a[href]");
1241
+ if (!a || !/^https?:/.test(a.href) || !frames(a.href))
1242
+ return;
1243
+ e.preventDefault();
1244
+ deck.open({ kind: PAGE, title: tabName(a.href), url: a.href });
1245
+ setPanel(true);
1246
+ },
1214
1247
  // Entered and left once for every child crossed, so the veil follows a
1215
1248
  // count rather than the last event.
1216
1249
  onDragEnter: (e) => {
@@ -1235,7 +1268,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1235
1268
  e.preventDefault();
1236
1269
  // A dropped folder arrives as entries; each file under it goes up with its path.
1237
1270
  void dropped(e.dataTransfer).then((list) => hold(at, list, via));
1238
- }, children: [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, children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { at: at, said: said, held: files, deck: deck, onOpen: () => setPanel(true), onCreate: () => window.location.assign(site("/dev")) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs" }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs" }) : 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,
1271
+ }, children: [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, children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { at: at, said: said, held: files, deck: deck, onOpen: () => setPanel(true), onCreate: () => window.location.assign(site("/dev")) }), _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,
1239
1272
  // AN EMPTY ROOM CENTRES ITS COMPOSER. While nothing has been said the
1240
1273
  // opening (the heading and the starters) and the composer sit as one
1241
1274
  // group in the middle of the height, with or without the plan bars in
@@ -1313,6 +1346,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1313
1346
  // `label` keeps the accessible name constant while it does.
1314
1347
  placeholder: room.length ? `Message ${join(room)}` : placeholder,
1315
1348
  label: ASK,
1349
+ note: caution(),
1350
+ // A phone's pill is the tall one: the field across the top, the controls under it.
1351
+ narrow: !media.sm,
1316
1352
  // In the app's frame the composer floats on the pane as a pane of its own.
1317
1353
  framed,
1318
1354
  // A file or an image pasted into the field is held like a picked one;
package/dist/Me.d.ts CHANGED
@@ -19,10 +19,12 @@ export declare function Face({ host, size }: {
19
19
  * plan, Free included, is one to move up from.
20
20
  */
21
21
  export declare const upgrade: (family: string | null | undefined) => string;
22
- export declare function Me({ host, narrow, onOrg, }: {
22
+ export declare function Me({ host, narrow, above, onOrg, }: {
23
23
  host: Host;
24
24
  /** The rail's: the face alone, the menu opening beside it. */
25
25
  narrow?: boolean;
26
+ /** The menu opens above the face, not beside it: the rail laid along a phone drawer's foot. */
27
+ above?: boolean;
26
28
  /** Work in another organization: picked, then the app opened in it. */
27
29
  onOrg: (org: string) => void;
28
30
  }): import("react").JSX.Element;
package/dist/Me.js CHANGED
@@ -162,7 +162,7 @@ const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(nav
162
162
  const Keys = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: children }));
163
163
  /** A row that opens a page of the menu. */
164
164
  const More = () => _jsx(ChevronRight, { size: 14, "aria-hidden": true, opacity: 0.6 });
165
- export function Me({ host, narrow = false, onOrg, }) {
165
+ export function Me({ host, narrow = false, above = false, onOrg, }) {
166
166
  const menu = useWho();
167
167
  const [page, setPage] = useState('main');
168
168
  const [creating, setCreating] = useState(false);
@@ -211,7 +211,7 @@ export function Me({ host, narrow = false, onOrg, }) {
211
211
  if (open)
212
212
  setPage('main');
213
213
  menu.onOpenChange(open);
214
- }, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: narrow ? undefined : '100%', children: _jsxs(XStack, { ref: (el) => {
214
+ }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: narrow ? undefined : '100%', children: _jsxs(XStack, { ref: (el) => {
215
215
  row.current = el instanceof HTMLElement ? el : null;
216
216
  }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: toggle, 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 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$5", p: "$1", width: 240, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(Refit, { children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: page === 'main' ? (_jsxs(YStack, { gap: 1, children: [_jsxs(XStack, { items: "center", gap: "$2.5", px: "$2", pt: "$1.5", pb: "$2", 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 || '' })] })] }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage remaining", trail: _jsx(More, {}), onPress: () => turn('usage') }), _jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: here?.display ?? current ?? 'Organization', sub: "Organization", trail: _jsx(More, {}), onPress: () => turn('orgs') }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", trail: _jsx(Keys, { children: apple ? '⌘,' : 'Ctrl+,' }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Help", trail: _jsx(More, {}), onPress: () => turn('help') }), host.signOut ? _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) }) : null] })) : page === 'usage' ? (_jsxs(YStack, { gap: 1, children: [back('Usage remaining'), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: upgrade(read.value?.label?.name), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) })] })) : page === 'orgs' ? (_jsxs(YStack, { gap: 1, children: [back('Organization'), _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: () => {
217
217
  menu.onOpenChange(false);
package/dist/Rail.js CHANGED
@@ -81,5 +81,5 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
81
81
  { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
82
82
  ];
83
83
  const Stack = flat ? XStack : YStack;
84
- return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) }), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: onOrg })] }));
84
+ return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) }), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
85
85
  }
package/dist/Recents.js CHANGED
@@ -78,7 +78,7 @@ function List({ mode, rows, words, view, keep, open, reading, error, onOpen, thr
78
78
  return;
79
79
  e.preventDefault?.();
80
80
  onOpen(r);
81
- }, onPress: () => onOpen(r), children: _jsx(SizableText, { size: "$3", color: open(r) ? '$ink' : '$quiet', numberOfLines: 1, children: r.title }) }));
81
+ }, onPress: () => onOpen(r), children: _jsx(SizableText, { size: "$3", color: open(r) ? '$ink' : '$quiet', numberOfLines: 1, display: "block", children: r.title }) }));
82
82
  if (t)
83
83
  return (_jsx(ThreadRow, { t: t, active: open(r), children: item }, r.id));
84
84
  return (_jsxs(YStack, { role: "listitem", position: "relative", children: [item, r.kind === 'dev' ? (_jsx(XStack, { position: "absolute", r: "$2.5", t: 0, b: 0, items: "center", pointerEvents: "none", children: _jsx(StatusDot, { status: DOTS[r.status] ?? 'idle' }) })) : null] }, r.id));
@@ -90,10 +90,13 @@ function List({ mode, rows, words, view, keep, open, reading, error, onOpen, thr
90
90
  const loose = groups.filter((g) => g.title !== PINNED);
91
91
  const pinned = groups.find((g) => g.title === PINNED);
92
92
  const note = reading ? (_jsx(Note, { children: mode === 'chat' ? 'Reading your chats…' : 'Reading your runs…' })) : error ? null : words.trim() ? (_jsx(Note, { children: `Nothing matches “${words.trim()}”.` })) : view.status === 'archived' ? (_jsx(Note, { children: "No archived chats." })) : narrowed ? (_jsx(Note, { children: "Nothing in this view." })) : (_jsx(Note, { children: mode === 'chat' ? 'No chats yet.' : 'No runs yet.' }));
93
- const body = (_jsxs(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: [pinned ? (_jsxs(YStack, { role: "listitem", gap: 1, children: [heading(PINNED), _jsx(YStack, { role: "list", "aria-label": PINNED, gap: 1, children: pinned.rows.map(row) })] })) : null, _jsxs(YStack, { role: "listitem", gap: 1, children: [heading('Recents', label, true), loose.length
94
- ? loose.map((g) => g.title ? (_jsxs(YStack, { gap: 1, pt: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g.title }), _jsx(YStack, { role: "list", "aria-label": g.title, gap: 1, children: g.rows.map(row) })] }, g.title)) : (_jsx(YStack, { role: "list", "aria-label": "All", gap: 1, children: g.rows.map(row) }, "all")))
95
- : pinned
96
- ? null
97
- : note] })] }));
98
- return (_jsx(YStack, { "data-slot": "recents", flex: 1, minH: 0, children: _jsxs(ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: [error ? _jsx(Note, { children: error.message }) : null, threads ? (_jsx(ConversationMenu, { threads: threads, active: held, onGone: onNew, children: _jsx(YStack, { children: body }) })) : (body)] }) }));
93
+ // Each list of conversations holds its own menu (thread.tsx): the view menu
94
+ // in the Recents heading between them is a menu of its own, never inside one.
95
+ const menus = (rows) => threads ? (_jsx(ConversationMenu, { threads: threads, active: held, onGone: onNew, children: _jsx(YStack, { children: rows }) })) : (rows);
96
+ const rest = loose.length
97
+ ? loose.map((g) => g.title ? (_jsxs(YStack, { gap: 1, pt: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g.title }), _jsx(YStack, { role: "list", "aria-label": g.title, gap: 1, children: g.rows.map(row) })] }, g.title)) : (_jsx(YStack, { role: "list", "aria-label": "All", gap: 1, children: g.rows.map(row) }, "all")))
98
+ : pinned
99
+ ? null
100
+ : note;
101
+ return (_jsx(YStack, { "data-slot": "recents", flex: 1, minH: 0, children: _jsxs(ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: [error ? _jsx(Note, { children: error.message }) : null, _jsxs(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: [pinned ? (_jsxs(YStack, { role: "listitem", gap: 1, children: [heading(PINNED), menus(_jsx(YStack, { role: "list", "aria-label": PINNED, gap: 1, children: pinned.rows.map(row) }))] })) : null, _jsxs(YStack, { role: "listitem", gap: 1, children: [heading('Recents', label, true), loose.length ? menus(rest) : rest] })] })] }) }));
99
102
  }
@@ -53,3 +53,11 @@ export declare function Summary({ at, said, held, deck, onOpen, onCreate }: {
53
53
  onOpen: () => void;
54
54
  onCreate: () => void;
55
55
  }): import("react").JSX.Element;
56
+ /**
57
+ * The organization's sites, as an empty tab suggests them: `GET /v1/projects`,
58
+ * every row with a live address. Read once a conversation; nothing on a refusal.
59
+ */
60
+ export declare function useSites(): {
61
+ title: string;
62
+ url: string;
63
+ }[];
package/dist/RightPane.js CHANGED
@@ -16,7 +16,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
16
16
  // The panel's tabs, their order and the open one are kept per conversation by
17
17
  // @hanzo/build (`useDeck`, scope `chat:<channel>`); the frame owns the column
18
18
  // it stands in (`Beside` in `Shell.tsx`).
19
- import { useMemo, useState } from 'react';
19
+ import { useEffect, useMemo, useState } from 'react';
20
20
  import { FileCode, Globe, Image as ImageMark, ListTodo, Paperclip, Plus, Search } from 'lucide-react';
21
21
  import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Popover, PopoverContent, PopoverTrigger, Text, XStack, YStack, } from '@hanzo/ui';
22
22
  import { useAi } from '@hanzo/ai/react';
@@ -24,7 +24,9 @@ import { artifacts, name as titleOf, PAGE } from '@hanzo/build';
24
24
  import { Card, Quiet, Row } from './card.js';
25
25
  import { said as told } from './lib/attach.js';
26
26
  import { download, fileOf } from './lib/files.js';
27
- import { org } from './lib/session.js';
27
+ import { org, scope } from './lib/session.js';
28
+ import { api } from './lib/api.js';
29
+ import { list } from './lib/list.js';
28
30
  import { localOf, take } from './pane.js';
29
31
  import { useProviders, useSkills } from './tools.js';
30
32
  import { activate, startDevice } from './connect.js';
@@ -251,3 +253,24 @@ export function Summary({ at, said, held, deck, onOpen, onCreate }) {
251
253
  return (_jsx(Line, { icon: _jsx(Glyph, { size: 15, "aria-hidden": true }), onPress: () => show(m), children: m.name }, m.id));
252
254
  }), !done.length && !took.length ? _jsx(Line, { quiet: true, children: "Nothing yet" }) : null] }) })] }));
253
255
  }
256
+ /**
257
+ * The organization's sites, as an empty tab suggests them: `GET /v1/projects`,
258
+ * every row with a live address. Read once a conversation; nothing on a refusal.
259
+ */
260
+ export function useSites() {
261
+ const [rows, setRows] = useState([]);
262
+ useEffect(() => {
263
+ const stop = new AbortController();
264
+ fetch(`${api()}/v1/projects`, { headers: scope(), signal: stop.signal })
265
+ .then((r) => (r.ok ? r.json() : null))
266
+ .then((body) => {
267
+ if (stop.signal.aborted)
268
+ return;
269
+ const all = list(Array.isArray(body) ? body : body?.data);
270
+ setRows(all.filter((p) => typeof p.liveUrl === 'string' && /^https:\/\//.test(p.liveUrl)).map((p) => ({ title: p.name || p.slug || 'Site', url: p.liveUrl })));
271
+ })
272
+ .catch(() => { });
273
+ return () => stop.abort();
274
+ }, []);
275
+ return rows;
276
+ }
@@ -55,6 +55,9 @@ export declare function createInvitation(org: string, email?: string, quota?: nu
55
55
  *
56
56
  * The organization is named as `?owner=`, the one input IAM's list reads. A
57
57
  * read that names none is authorized against no organization and refused.
58
+ *
59
+ * A listed invitation carries no code: IAM shows a code once, in the answer to
60
+ * `createInvitation`. A row is an invitation by its name.
58
61
  */
59
62
  export declare function listInvitations(org: string): Promise<Invitation[]>;
60
63
  /**
@@ -87,10 +87,13 @@ export async function createInvitation(org, email, quota = 1) {
87
87
  *
88
88
  * The organization is named as `?owner=`, the one input IAM's list reads. A
89
89
  * read that names none is authorized against no organization and refused.
90
+ *
91
+ * A listed invitation carries no code: IAM shows a code once, in the answer to
92
+ * `createInvitation`. A row is an invitation by its name.
90
93
  */
91
94
  export async function listInvitations(org) {
92
95
  const body = (await call(`/v1/iam/invitations?owner=${encodeURIComponent(org)}`));
93
- return (body.invitations ?? []).filter((r) => r && r.code);
96
+ return (body.invitations ?? []).filter((r) => r && r.name);
94
97
  }
95
98
  /**
96
99
  * Emails the join link to the address an invitation is pinned to, and returns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.58",
3
+ "version": "0.1.61",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,7 +66,7 @@
66
66
  "devDependencies": {
67
67
  "@hanzo/ai": "0.6.23",
68
68
  "@hanzo/appearance": "0.2.8",
69
- "@hanzo/build": "0.2.53",
69
+ "@hanzo/build": "0.2.56",
70
70
  "@hanzo/composer": "0.2.0",
71
71
  "@hanzo/design": "0.5.34",
72
72
  "@hanzo/event": "0.3.56",