@hanzo/rooms 0.1.58 → 0.1.59

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. */
@@ -507,6 +510,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
507
510
  const wide = useMedia().lg;
508
511
  const [panel, setPanel] = usePanel("chat", false, wide);
509
512
  const deck = useDeck(`chat:${at}`, SEED);
513
+ // The org's sites, which an empty tab suggests.
514
+ const sites = useSites();
510
515
  useKey(() => setPanel(!panel));
511
516
  // On a laptop the frame's column is the panel's; a close from the frame (its
512
517
  // edge pulled shut, the shell's own control) comes back into `panel`.
@@ -1210,7 +1215,34 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1210
1215
  }, onCancel: () => {
1211
1216
  setAsking(null);
1212
1217
  } }));
1218
+ /** 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. */
1219
+ const browsing = {
1220
+ suggested: sites,
1221
+ onAsk: (page) => {
1222
+ const quote = page.selection ? `\n> ${page.selection.trim().split("\n").join("\n> ")}` : "";
1223
+ setDraft((was) => `${was ? `${was}\n\n` : ""}About ${page.title} (${page.url}):${quote}\n`);
1224
+ },
1225
+ onCapture: (file) => void hold(at, [file], via),
1226
+ onDev: (page) => {
1227
+ const slug = new URL(page.url).hostname.match(/^([a-z0-9-]+)\.hanzo\.app$/)?.[1] ?? null;
1228
+ window.location.assign(dev(slug));
1229
+ },
1230
+ };
1213
1231
  return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
1232
+ // A LINK IN AN ANSWER OPENS IN A TAB beside the conversation when the tab
1233
+ // can show it — a site or preview we host — and in a browser tab of its
1234
+ // own otherwise, as a modified click always does.
1235
+ onClickCapture: (press) => {
1236
+ const e = press;
1237
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
1238
+ return;
1239
+ const a = e.target?.closest?.("a[href]");
1240
+ if (!a || !/^https?:/.test(a.href) || !frames(a.href))
1241
+ return;
1242
+ e.preventDefault();
1243
+ deck.open({ kind: PAGE, title: tabName(a.href), url: a.href });
1244
+ setPanel(true);
1245
+ },
1214
1246
  // Entered and left once for every child crossed, so the veil follows a
1215
1247
  // count rather than the last event.
1216
1248
  onDragEnter: (e) => {
@@ -1235,7 +1267,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1235
1267
  e.preventDefault();
1236
1268
  // A dropped folder arrives as entries; each file under it goes up with its path.
1237
1269
  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,
1270
+ }, 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
1271
  // AN EMPTY ROOM CENTRES ITS COMPOSER. While nothing has been said the
1240
1272
  // opening (the heading and the starters) and the composer sit as one
1241
1273
  // group in the middle of the height, with or without the plan bars in
@@ -1313,6 +1345,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1313
1345
  // `label` keeps the accessible name constant while it does.
1314
1346
  placeholder: room.length ? `Message ${join(room)}` : placeholder,
1315
1347
  label: ASK,
1348
+ note: caution(),
1316
1349
  // In the app's frame the composer floats on the pane as a pane of its own.
1317
1350
  framed,
1318
1351
  // 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
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.55",
70
70
  "@hanzo/composer": "0.2.0",
71
71
  "@hanzo/design": "0.5.34",
72
72
  "@hanzo/event": "0.3.56",