@hanzo/rooms 0.1.84 → 0.1.86

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
@@ -69,7 +69,7 @@ import { isPlans, leave, useDevHost } from './dev.js';
69
69
  import { Orgs } from './Orgs.js';
70
70
  import { useRooms } from './host.js';
71
71
  import { useKept } from './kept.js';
72
- import { Mark, Rail, Toggle } from './Rail.js';
72
+ import { Mark, Rail, spot, Toggle } from './Rail.js';
73
73
  import { empty, openThread, useOpen } from './open.js';
74
74
  import { Paned } from './Shell.js';
75
75
  import { Palette } from './Palette.js';
@@ -191,6 +191,14 @@ function Window({ bar }) {
191
191
  last[mode] = `${window.location.pathname}${window.location.search}`;
192
192
  setDrawer(false);
193
193
  }, [mode, here]);
194
+ // Where the reader was before a place that takes the sidebar (Library, Scheduled
195
+ // tasks, Customize, Settings): its Back returns there, Chat or Dev alike.
196
+ const away = useRef('');
197
+ const inPlace = mode === 'dev' && spot(where) !== null;
198
+ useEffect(() => {
199
+ if (!inPlace)
200
+ away.current = `${window.location.pathname}${window.location.search}`;
201
+ }, [inPlace, here]);
194
202
  /** A move of the app's own: the drawer away, then the address. */
195
203
  const move = useCallback((address) => {
196
204
  shut();
@@ -227,6 +235,7 @@ function Window({ bar }) {
227
235
  window.addEventListener('keydown', key);
228
236
  return () => window.removeEventListener('keydown', key);
229
237
  }, [bar, router, move]);
238
+ const back = () => move(away.current || app(''));
230
239
  const toChat = () => move(last.chat || talk());
231
240
  const toDev = () => move(last.dev || app(''));
232
241
  const toStudio = () => move(last.studio || studio(''));
@@ -241,5 +250,5 @@ function Window({ bar }) {
241
250
  shut();
242
251
  setFinding(true);
243
252
  };
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] }) }) }) }));
253
+ 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, onBack: back, 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, onBack: back, 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, mode: mode, host: host }) : null] }) }) }) }));
245
254
  }
package/dist/Chat.js CHANGED
@@ -44,7 +44,7 @@ import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
44
44
  import { useMedia } from "@hanzo/gui";
45
45
  import { tidy } from "./lib/title.js";
46
46
  import { useIam, useIamToken } from "@hanzo/iam/react";
47
- import { bearer, hasSession, org } from "./lib/session.js";
47
+ import { bearer, hasSession, named, org } from "./lib/session.js";
48
48
  import { api } from "./lib/api.js";
49
49
  import { app, dev, go } from "./lib/host.js";
50
50
  import { useRooms } from "./host.js";
@@ -64,7 +64,7 @@ import { Share } from "./Share.js";
64
64
  import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
65
65
  import { Schedule } from "./Schedule.js";
66
66
  import { keepFile, useAttach, useKeep } from "./keep.js";
67
- import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, usePanel, frames, name as tabName, } from "@hanzo/build";
67
+ import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
68
68
  import { checkoutUrl } from './lib/pay.js';
69
69
  import { Upgrade, useFree } from './Upgrade.js';
70
70
  import { enter, LOGIN } from './lib/destination.js';
@@ -224,8 +224,6 @@ export function Ask({ placeholder, idle, }) {
224
224
  * accessible name does not move when the placeholder does.
225
225
  */
226
226
  const message = () => `Ask ${brand()}`;
227
- /** The line every answer is sent under, above the composer, as the reference sets it. */
228
- const caution = () => `${brand()} can make mistakes. Check important info.`;
229
227
  /** The panel offers no kinds of its own in Chat: its tabs are pages. */
230
228
  const NONE = [];
231
229
  /** The id the room's system turn is held under in the thread. */
@@ -527,8 +525,10 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
527
525
  setDraft: (x) => lent.current.setDraft(x),
528
526
  attach: (f) => lent.current.attach(f),
529
527
  go: (href) => (/^https?:/.test(href) ? void window.open(href, "_blank", "noopener,noreferrer") : go(href)),
530
- });
528
+ }, CHAT_MODES);
531
529
  const agentShelf = useAgentShelf();
530
+ // The opening greets the person by their first name, as their account spells it.
531
+ const firstName = useMemo(() => (anonymous ? "" : ((named()?.name ?? "").trim().split(/\s+/)[0] ?? "")), [anonymous]);
532
532
  // Up to three things worth doing next, from the person's own activity (@hanzo/build).
533
533
  const hints = useSuggestions(target);
534
534
  // THE HEADER'S ⋯ IS THE ROW'S MENU (thread.tsx): rename, pin, share, its
@@ -1362,7 +1362,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1362
1362
  ready: Boolean(draft.trim() || files.length),
1363
1363
  // `label` keeps the accessible name constant; the placeholder is the composer's own.
1364
1364
  label: ASK,
1365
- note: caution(),
1366
1365
  // A phone's pill is the tall one: the field across the top, the controls under it.
1367
1366
  narrow: !media.sm,
1368
1367
  // In the app's frame the composer floats on the pane as a pane of its own.
@@ -1411,7 +1410,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1411
1410
  ],
1412
1411
  slash: anonymous ? undefined : carry.skills,
1413
1412
  at: anonymous ? undefined : carry.plugins,
1414
- mode: anonymous ? null : { value: carry.gear.gear.mode, set: (m) => carry.gear.set({ mode: m }) },
1413
+ mode: anonymous ? null : carry.mode,
1415
1414
  // The model, the effort and the mode under the frame (@hanzo/build).
1416
1415
  mind,
1417
1416
  limits,
@@ -1530,10 +1529,10 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1530
1529
  // with it when it did. `Ask` passes its own, because the page embedding
1531
1530
  // it already has an `<h1>` and a second is the defect that took the
1532
1531
  // landings off /chat and /dev.
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
+ empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", pt: "$6", pb: "$2", ...(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 || (firstName ? `What’s up, ${firstName}?` : "What’s up?") }), 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] })),
1534
1533
  // The transcript reads at the same measure. `Thread` defaults to 768
1535
1534
  // itself; handing it the column here is what lets Width reach it.
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
+ column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: isFree(model) ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models.", " ", _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] })) : (_jsxs(_Fragment, { 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) })] }));
1537
1536
  }
1538
1537
  /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1539
1538
  function Consent({ question, onAgree, onCancel, }) {
package/dist/Column.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type Host } from '@hanzo/build';
2
2
  import { type RailLink } from '@hanzo/ui/chat';
3
3
  import { type ReactNode } from 'react';
4
4
  import { type Place } from './lib/host.js';
5
- export declare function Column({ host, mode, thread, go, onShut, onSeek, drawer, foot, places, }: {
5
+ export declare function Column({ host, mode, thread, go, onShut, onSeek, onBack, drawer, foot, places, }: {
6
6
  host: Host;
7
7
  mode: Place['mode'];
8
8
  /** The Chat conversation the pane holds, or null. */
@@ -13,6 +13,8 @@ export declare function Column({ host, mode, thread, go, onShut, onSeek, drawer,
13
13
  onShut: () => void;
14
14
  /** Opens the palette. */
15
15
  onSeek?: () => void;
16
+ /** Leaves a place that took the sidebar, for where the reader was before it. */
17
+ onBack: () => void;
16
18
  /** Drawn in the phone's drawer: no edge, cut as a floating pane. */
17
19
  drawer?: boolean;
18
20
  /** What stands at its foot: the drawer's rail, laid flat. */
package/dist/Column.js CHANGED
@@ -12,21 +12,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  // that moved last, each one press from its page, and Notifications in Settings.
13
13
  // The search opens the palette (Palette.tsx; ⌘K / Ctrl+K from anywhere).
14
14
  //
15
- // Dev's places are @hanzo/build's `nav`, the one list every rail draws, each a
16
- // row under a quiet group label.
15
+ // Dev's places are @hanzo/build's `own`: New run, Projects, Issues, Templates.
16
+ // A place the rail holds (Library, Scheduled tasks, Customize) and Settings
17
+ // each take the sidebar instead (Rail.tsx `spot`): Back, the place's name, and
18
+ // its own rows (Settings' sections, Customize's tabs), never Dev's beside them.
17
19
  //
18
20
  // Its width is the edge's in the gutter to its right: dragged, keyed or
19
21
  // double-clicked back (@hanzo/build's `Grip`), kept per browser. Pulled in past
20
22
  // its floor it shuts (`onShut`), as the sidebar's toggle shuts it.
21
23
  import { Text, XStack, YStack } from '@hanzo/gui';
22
- import { Bell, Search, Settings2, SquarePen } from 'lucide-react';
23
- import { DOTS, Grip, nav, NEW, path as written, useSessions } from '@hanzo/build';
24
+ import { ArrowLeft, Bell, Search, Settings2, SquarePen } from 'lucide-react';
25
+ import { DOTS, Gutter, NEW, own, path as written, sections, tabs, useSessions } from '@hanzo/build';
24
26
  import { tidy } from './lib/title.js';
25
27
  import { DropdownMenu } from '@hanzo/ui';
26
28
  import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
27
29
  import { useId } from 'react';
28
30
  import { Made } from './Me.js';
29
31
  import { Recents } from './Recents.js';
32
+ import { spot } from './Rail.js';
30
33
  import { Setup } from './Setup.js';
31
34
  import { ago } from './recent.js';
32
35
  import { empty } from './open.js';
@@ -34,9 +37,6 @@ import { useSpan } from './span.js';
34
37
  import { pane } from './ground.js';
35
38
  import { talk } from './lib/host.js';
36
39
  import { brand, wordmark } from './where.js';
37
- /** The grip stands in the gutter between the sidebar and the panel. gui carries a `calc()` to the page unchanged; its
38
- * types only name custom properties. */
39
- const GUTTER_END = 'calc(-1 * var(--pane-gap))';
40
40
  /** The sidebar fills the column. Its `width` is typed in px alone, though its frame takes any gui size — the type is
41
41
  * @hanzo/ui's to widen. */
42
42
  const FILL = { width: '100%' };
@@ -69,6 +69,8 @@ const keys = (press) => ({
69
69
  const ROW = { rounded: '$4', minH: 31, py: 0, px: '$2' };
70
70
  /** The run activity behind the bell: the runs that moved last, newest first. */
71
71
  const RECENT = 6;
72
+ /** What a place that takes the sidebar is called at its head. */
73
+ const TITLE = { library: 'Library', scheduled: 'Scheduled tasks', customize: 'Customize', settings: 'Settings' };
72
74
  /** A header mark: a 16px glyph in a 32px target. */
73
75
  const HEAD = { width: 32, height: 32, rounded: '$3' };
74
76
  function Activity({ host }) {
@@ -90,12 +92,13 @@ function Activity({ host }) {
90
92
  ];
91
93
  return (_jsx(DropdownMenu, { minWidth: 280, trigger: _jsx(XStack, { render: "button", "aria-label": "Activity", ...{ title: 'Activity' }, ...HEAD, items: "center", justify: "center", cursor: "pointer", opacity: 0.7, hoverStyle: { bg: '$hover', opacity: 1 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Bell, { size: 16, "aria-hidden": true }) }), items: items }));
92
94
  }
93
- export function Column({ host, mode, thread, go, onShut, onSeek, drawer = false, foot, places, }) {
95
+ export function Column({ host, mode, thread, go, onShut, onSeek, onBack, drawer = false, foot, places, }) {
94
96
  // The desktop sidebar and a phone's drawer can both be in the page: each names its own group labels.
95
97
  const uid = useId();
96
98
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
97
99
  const dev = (p) => host.go(p);
98
- const groups = places ?? nav(host, dev);
100
+ const at = mode === 'dev' ? spot(host.path) : null;
101
+ const groups = places ?? (at === 'settings' ? sections(host, dev) : at === 'customize' ? tabs(host, dev) : at ? [] : own(host, dev));
99
102
  const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
100
103
  // Chat's New; Dev's is the first of its places.
101
104
  const create = () => {
@@ -104,7 +107,5 @@ export function Column({ host, mode, thread, go, onShut, onSeek, drawer = false,
104
107
  };
105
108
  return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
106
109
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
107
- style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), _jsx(SidebarIconButton, { label: "Search", onPress: onSeek, "aria-haspopup": "dialog", ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) })] }), mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i ? `${uid}-${g.id}` : undefined, "aria-label": i ? undefined : g.label, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), mode === 'studio' ? null : _jsx(Recents, { host: host, mode: mode, thread: thread, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), foot] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar",
108
- // In the gutter between the sidebar and the panel, not over the sidebar's own rows.
109
- r: GUTTER_END, width: "var(--pane-gap)" }))] }));
110
+ style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [at ? (_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: 2, pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(SidebarIconButton, { label: "Back", "data-slot": "place-back", onPress: onBack, ...HEAD, children: _jsx(ArrowLeft, { size: 16, "aria-hidden": true }) }), _jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, "data-slot": "place-name", children: TITLE[at] })] })) : (_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), _jsx(SidebarIconButton, { label: "Search", onPress: onSeek, "aria-haspopup": "dialog", ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) })] })), mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i && g.label ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i && g.label ? `${uid}-${g.id}` : undefined, "aria-label": i && g.label ? undefined : g.label || (at ? TITLE[at] : g.id), gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), mode === 'studio' || at ? null : _jsx(Recents, { host: host, mode: mode, thread: thread, onOpen: open, onNew: create }), at ? null : (_jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] })), foot] }), drawer ? null : (_jsx(Gutter, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar" }))] }));
110
111
  }
package/dist/Me.js CHANGED
@@ -39,6 +39,7 @@ import { list } from './lib/list.js';
39
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
+ import { payUrl } from './lib/pay.js';
42
43
  import { asked, KEYS, legal, out, pages } from './help.js';
43
44
  import { showSettings } from './open.js';
44
45
  import { site, where } from './where.js';
@@ -146,9 +147,20 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
146
147
  const target = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
147
148
  const read = useStanding(target, open);
148
149
  const other = usePlan(personal && personal.name !== current ? personal.name : null, open);
150
+ // THE TWO ACCOUNTS, apart: the organization's plan (hanzo · Enterprise) and the
151
+ // person's own (Personal · Pro, or none and the way to one). A business plan is
152
+ // the organization's; a personal plan is held beside it, never instead of it.
153
+ const team = here && !here.personal ? here : (all.find((o) => !o.personal) ?? null);
154
+ const teamOther = usePlan(team && team.name !== current ? team.name : null, open);
149
155
  const planFor = (o) => (o.name === current ? spoken(read.value?.label ?? null) : o.personal ? other : '');
150
156
  const who = host.person?.name || host.person?.email || '';
151
157
  const plan = spoken(read.value?.label ?? null);
158
+ const teamPlan = team ? (team.name === current ? plan : teamOther) : '';
159
+ const ownPlan = personal ? (personal.name === current ? plan : other) : '';
160
+ // Where the reader works, and on what: "hanzo · Enterprise", "Personal · Pro".
161
+ const working = [here ? (here.personal ? 'Personal' : here.display) : current, plan].filter(Boolean).join(' · ');
162
+ // A personal plan is bought for the personal organization, beside any business one.
163
+ const getPro = () => personal && window.location.assign(payUrl('/cart', { plan: 'pro', org: personal.name, returnUrl: window.location.href }));
152
164
  const apple = mac();
153
165
  useEffect(() => {
154
166
  if (above)
@@ -180,9 +192,11 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
180
192
  return;
181
193
  setAccount(false);
182
194
  setHelp(false);
183
- }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
195
+ }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start',
196
+ // Clear of the rail's mark (or the row) it opens from, and flush with its foot.
197
+ offset: 8, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
184
198
  row.current = el instanceof HTMLElement ? el : 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 })] }));
199
+ }, 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 }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : 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: host.person.email || working })] })] }), 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] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : 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 })] }));
186
200
  }
187
201
  /** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
188
202
  function Shortcuts({ open, onOpenChange, apple }) {
package/dist/Orgs.js CHANGED
@@ -47,7 +47,8 @@ import { matches, refusal, slugify } from './lib/orgname.js';
47
47
  import { team as workspace } from './lib/host.js';
48
48
  import { checkoutUrl, money } from './lib/pay.js';
49
49
  import { cartUrl, charge, priced, usePlan, usePlans } from './lib/plans.js';
50
- import { createInvitation, inviteLink } from './lib/invite.js';
50
+ import { createInvitation, inviteLink, setOrganizationProfile } from './lib/invite.js';
51
+ import { createOrg, say } from './lib/org.js';
51
52
  import { countSignup } from './lib/intent.js';
52
53
  import { BAD, mix } from './lib/mix.js';
53
54
  import { useRooms } from './host.js';
@@ -647,12 +648,15 @@ function Everyone({ q, skip }) {
647
648
  *
648
649
  * The slug follows the name the way IAM's onboarding makes it, until it is typed
649
650
  * 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.
651
+ * refused here with the rule, never rewritten. The create is the one every
652
+ * surface uses, `POST /v1/account/orgs` (lib/org.ts `createOrg`): it files the
653
+ * organization in IAM and records the reader as its owner through IAM's
654
+ * membership API, the account untouched. IAM's own `/v1/iam/onboard` is not
655
+ * reachable from here: the edge answers no app origin's CORS on it, because it
656
+ * re-keys the session (cloud cors_origin.go `sessionRoutes`). It is sent the
657
+ * name when the name makes the slug, and the slug when the slug was typed, the
658
+ * display name then written on the row (lib/invite.ts `setOrganizationProfile`).
659
+ * The token in hand predates it, so it is refreshed before the organization opens.
656
660
  */
657
661
  function Found({ onClose, onMade }) {
658
662
  const { sdk } = useIam();
@@ -681,29 +685,15 @@ function Found({ onClose, onMade }) {
681
685
  return setRefused(wrong);
682
686
  setBusy(true);
683
687
  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
688
  try {
690
689
  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.');
690
+ const made = await createOrg(named ? display : slug);
697
691
  renew();
698
692
  await sdk.refreshAccessToken().catch(() => null);
699
693
  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 }),
694
+ await setOrganizationProfile('admin', made, { displayName: display }).catch((err) => {
695
+ throw new Error(`${made} is made, and it is still called ${made}: ${say(err)}`);
704
696
  });
705
- if (!p.ok)
706
- throw new Error(`${made} is made, and it is still called ${made}: ${await why(p)}`);
707
697
  }
708
698
  // A token that does not carry it yet is minted on the next load, which opens it.
709
699
  if (!claimed().includes(made)) {
@@ -714,7 +704,7 @@ function Found({ onClose, onMade }) {
714
704
  onMade(made);
715
705
  }
716
706
  catch (err) {
717
- setRefused(err instanceof Error ? err.message : 'Could not create it.');
707
+ setRefused(say(err));
718
708
  setBusy(false);
719
709
  }
720
710
  };
package/dist/Rail.d.ts CHANGED
@@ -3,6 +3,19 @@ import { type Host } from '@hanzo/build';
3
3
  import { type Place } from './lib/host.js';
4
4
  /** The rail's width, px. */
5
5
  export declare const RAIL = 48;
6
+ /** The places the rail holds beside the products, by the builder address each opens. */
7
+ export declare const RAISED: {
8
+ readonly library: '-/artifacts';
9
+ readonly scheduled: '-/automations';
10
+ readonly customize: '-/customize';
11
+ };
12
+ export type Raised = keyof typeof RAISED;
13
+ /** A place that takes the sidebar: one of the rail's, or Settings (the account menu's). */
14
+ export type Spot = Raised | 'settings';
15
+ /** The place a builder address stands on, if it is one that takes the sidebar. */
16
+ export declare const spot: (at: string | null) => Spot | null;
17
+ /** Which of them a builder address stands on, if any: an agent's page is Customize's, an automation Scheduled tasks'. */
18
+ export declare function raised(at: string | null): Raised | null;
6
19
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
7
20
  export declare function Mark({ label, on, onPress, children, ...rest }: {
8
21
  label: string;
@@ -18,9 +31,11 @@ export declare function Toggle({ open, onPress, label }: {
18
31
  onPress: () => void;
19
32
  label?: string;
20
33
  }): import("react").JSX.Element;
21
- export declare function Rail({ host, mode, open, toggle, flat, onChat, onDev, onStudio, onOrg, }: {
34
+ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, onStudio, go, onOrg, }: {
22
35
  host: Host;
23
36
  mode: Place['mode'];
37
+ /** Dev's builder address, or null outside Dev. */
38
+ at: string | null;
24
39
  /** The sidebar beside the rail is open. */
25
40
  open: boolean;
26
41
  /** Lead with the sidebar's toggle; absent where the host's bar holds it. */
@@ -31,6 +46,8 @@ export declare function Rail({ host, mode, open, toggle, flat, onChat, onDev, on
31
46
  onChat: () => void;
32
47
  onDev: () => void;
33
48
  onStudio: () => void;
49
+ /** Moves the app to an address of this page. */
50
+ go: (address: string) => void;
34
51
  /** Work in another organization. */
35
52
  onOrg: (org: string) => void;
36
53
  }): import("react").JSX.Element;
package/dist/Rail.js CHANGED
@@ -1,19 +1,20 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The app's rail: its products as marks down the window's left edge, in one
4
- // order — Chat, Dev, Studio — then More, a menu of the products on hosts of
5
- // their own, and the account at the foot. It is how the app moves between its
6
- // products; a phone, which has no rail, switches Chat and Dev in its bar
7
- // (Top.tsx).
4
+ // order — Chat, Dev, Studio, Library, Scheduled tasks, Customize — then More, a
5
+ // menu of the products on hosts of their own, and the account at the foot. It
6
+ // is how the app moves between its products; a phone, which has no rail,
7
+ // switches Chat and Dev in its bar (Top.tsx).
8
8
  //
9
9
  // Studio is the app's third mode (@hanzo/studio at /studio), each of the three
10
10
  // opened where the reader left it.
11
11
  //
12
- // A mark is a product, and a product owns the sidebar beside the rail: Chat its
13
- // conversations, Dev its places (@hanzo/build's New, Projects, Issues, Library,
14
- // Templates, Scheduled tasks, Machines, Environments, Customize, Docs) and its
15
- // runs, Studio its own. Nothing of Dev's is raised to the rail, and nothing of
16
- // the rail's is listed inside Dev: one level of the app a place.
12
+ // A mark is a place, and a place owns the sidebar beside the rail: Chat its
13
+ // conversations, Dev its own places (@hanzo/build `own`: New run, Projects,
14
+ // Issues, Templates) and its runs, Studio its own; Library, Scheduled tasks and
15
+ // Customize (@hanzo/build's Artifacts, Automations and Customize) each their
16
+ // own, and Settings its sections (Column.tsx). Nothing on the rail is listed
17
+ // inside Dev: one level of the app a place, each lit while the reader is on it.
17
18
  //
18
19
  // It stands on the frame's floor, never cut as a pane (ground.ts): the rail and
19
20
  // the sidebar beside it are the window, and the content is the panel inset in
@@ -22,17 +23,37 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
23
  // The sidebar's toggle leads it (`toggle`) where the host has no bar of its own:
23
24
  // a browser tab. A host that is a window draws the toggle in its bar beside Back
24
25
  // and Forward (App.tsx `bar`), and its rail starts with Chat. On a phone the
25
- // rail lies along the foot of the drawer (`flat`), the account at its end.
26
- import { Code2, Ellipsis, MessageSquare, PanelLeft, SquareTerminal, Users, WandSparkles } from 'lucide-react';
26
+ // rail lies along the foot of the drawer (`flat`), the account at its end; the
27
+ // drawer is too narrow for every mark, so Library, Scheduled tasks and
28
+ // Customize lead More there.
29
+ import { Clock, Code2, Ellipsis, Library, MessageSquare, PanelLeft, Puzzle, SquareTerminal, Users, WandSparkles } from 'lucide-react';
27
30
  import { XStack, YStack } from '@hanzo/gui';
31
+ import { route } from '@hanzo/build';
28
32
  import { DropdownMenu } from '@hanzo/ui';
29
33
  import { out } from './help.js';
30
34
  import { Me } from './Me.js';
31
- import { TABS, WORKSPACE } from './lib/host.js';
35
+ import { app, TABS, WORKSPACE } from './lib/host.js';
32
36
  /** The rail's width, px. */
33
37
  export const RAIL = 48;
34
38
  /** A mark, px: a 36px square on a 44px pitch. */
35
39
  const MARK = 36;
40
+ /** The places the rail holds beside the products, by the builder address each opens. */
41
+ export const RAISED = { library: '-/artifacts', scheduled: '-/automations', customize: '-/customize' };
42
+ /** The place a builder address stands on, if it is one that takes the sidebar. */
43
+ export const spot = (at) => (at !== null && route(at).kind === 'settings' ? 'settings' : raised(at));
44
+ /** Which of them a builder address stands on, if any: an agent's page is Customize's, an automation Scheduled tasks'. */
45
+ export function raised(at) {
46
+ if (at === null)
47
+ return null;
48
+ const r = route(at);
49
+ if (r.kind === 'screen' && r.screen === 'artifacts')
50
+ return 'library';
51
+ if ((r.kind === 'screen' && r.screen === 'automations') || r.kind === 'automation')
52
+ return 'scheduled';
53
+ if (r.kind === 'customize' || r.kind === 'agent' || (r.kind === 'screen' && r.screen === 'mcp'))
54
+ return 'customize';
55
+ return null;
56
+ }
36
57
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
37
58
  export function Mark({ label, on = false, onPress, children, ...rest }) {
38
59
  return (_jsx(XStack, { render: "button", "aria-label": label, ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
@@ -43,12 +64,22 @@ export function Mark({ label, on = false, onPress, children, ...rest }) {
43
64
  export function Toggle({ open, onPress, label }) {
44
65
  return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
45
66
  }
46
- export function Rail({ host, mode, open, toggle, flat = false, onChat, onDev, onStudio, onOrg, }) {
47
- // The products on hosts of their own, behind More.
67
+ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, onStudio, go, onOrg, }) {
68
+ const here = mode === 'dev' ? spot(at) : null;
69
+ const to = (p) => () => go(app(p));
70
+ // The products on hosts of their own, behind More; on a phone the raised places lead it.
48
71
  const rest = [
72
+ ...(flat
73
+ ? [
74
+ { key: 'library', label: 'Library', icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
75
+ { key: 'scheduled', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
76
+ { key: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
77
+ { type: 'separator', key: 'raised' },
78
+ ]
79
+ : []),
49
80
  { key: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }), onSelect: () => out(TABS) },
50
81
  { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
51
82
  ];
52
83
  const Stack = flat ? XStack : YStack;
53
- 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', onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { 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 })] }));
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: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), flat ? null : (_jsxs(_Fragment, { children: [_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 })] }));
54
85
  }
package/dist/Shell.js CHANGED
@@ -27,8 +27,7 @@ import { Box, Shortcut, XStack, YStack, Text } from "@hanzo/ui";
27
27
  import { scrim, sheet } from "@hanzo/ui/glass";
28
28
  import { Sidebar, SidebarIconButton, SidebarNewChat, SidebarScroll, SidebarSection, SidebarItem, SidebarFolder, } from "@hanzo/ui/chat";
29
29
  import { OrgProjectSwitcher, useIam, useOrganizations, } from "@hanzo/iam/react";
30
- import { SideProvider, useSplit } from "@hanzo/build";
31
- import { Grip } from "@hanzo/ui";
30
+ import { Gutter, SideProvider, useSplit } from "@hanzo/build";
32
31
  import { HanzoMark, } from "@hanzogui/shell";
33
32
  import { useHydrated } from "./lib/hydrated.js";
34
33
  import { useHost } from "./lib/hostname.js";
@@ -58,12 +57,8 @@ const FIELD = {
58
57
  color: "inherit",
59
58
  fontSize: "$2",
60
59
  };
61
- /** The side column's grip stands in the gutter the frame leaves, or 8px where there is none; the rail clears the
62
- * home indicator on a phone. gui carries a `calc()`, `max()` or `env()` to the page unchanged; its types only name
60
+ /** The rail clears the home indicator on a phone. gui carries an `env()` to the page unchanged; its types only name
63
61
  * custom properties. */
64
- const GUTTER_START = "calc(-1 * var(--pane-gap, 0px))";
65
- const GUTTER = "max(8px, var(--pane-gap, 8px))";
66
- const TOUCH_START = "calc(-22px - var(--pane-gap, 0px) / 2)";
67
62
  const HOME_INDICATOR = "env(safe-area-inset-bottom)";
68
63
  import { invite } from "./lib/home.js";
69
64
  import { reach } from "./lib/reach.js";
@@ -910,13 +905,7 @@ export function Paned({ children, swap, framed = false, bare = false, }) {
910
905
  // In the frame the gutter stands between the pane and this column.
911
906
  ml: framed ? "var(--pane-gap)" : undefined, ref: split.aside,
912
907
  // inline-style: the width a drag rewrites on the element itself, and full screen (useSplit).
913
- style: split.asideStyle, position: "relative", shrink: 0, minH: 0, display: "none", $lg: { display: "flex" }, children: [split.grip ? (_jsx(Grip, { ...split.grip, label: "Resize side panel", bar: "inline",
914
- // The handle halfway down the edge, where a hand looks for it.
915
- justify: "center",
916
- // In the gutter the frame leaves between the panes; a room has none.
917
- l: GUTTER_START, width: GUTTER,
918
- // A finger's band, centred on the gutter.
919
- $touchable: { l: TOUCH_START, width: 44 } })) : null, _jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", rounded: "var(--pane-round)", ref: (el) => setSlot(el instanceof HTMLElement ? el : null) })] })) : null] }));
908
+ style: split.asideStyle, position: "relative", shrink: 0, minH: 0, display: "none", $lg: { display: "flex" }, children: [split.grip ? (_jsx(Gutter, { ...split.grip, label: "Resize side panel", inset: 1 })) : null, _jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", rounded: "var(--pane-round)", ref: (el) => setSlot(el instanceof HTMLElement ? el : null) })] })) : null] }));
920
909
  }
921
910
  /**
922
911
  * THE RAIL: where you can go, as marks with their names under them. It is the
@@ -1229,7 +1218,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
1229
1218
  const list = (_jsxs(Sidebar, { ref: side.ref,
1230
1219
  // A phone's sheet is never wider than 280, whatever the laptop kept.
1231
1220
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
1232
- style: side.style, $md: { width: "var(--span)" }, "$max-md": { width: "100%", maxW: 280 }, ...sheet(0), borderRightWidth: 1, borderColor: "var(--edge, var(--white-06))", position: "relative", children: [_jsx(Grip, { side: "left", span: side.span, floor: FLOOR, ceil: CEIL, reset: SIDEBAR_WIDTH, onSpan: side.move, onKeep: side.keep, onShut: () => setAsked(false), label: "Resize sidebar" }), _jsx(XStack, { "data-slot": "mode-name", items: "center", px: "$2", pt: "$2", minW: 0, children: _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode === "dev" ? "dev" : "chat") }) }), _jsxs(XStack, { "data-slot": "workspace-switcher", items: "center", gap: "$2", px: "$2", pt: "$1", pb: "$1", minW: 0, children: [_jsx(Text, { render: _jsx("a", { href: "/", onClick: (e) => {
1221
+ style: side.style, $md: { width: "var(--span)" }, "$max-md": { width: "100%", maxW: 280 }, ...sheet(0), borderRightWidth: 1, borderColor: "var(--edge, var(--white-06))", position: "relative", children: [_jsx(Gutter, { side: "left", span: side.span, floor: FLOOR, ceil: CEIL, reset: SIDEBAR_WIDTH, onSpan: side.move, onKeep: side.keep, onShut: () => setAsked(false), label: "Resize sidebar" }), _jsx(XStack, { "data-slot": "mode-name", items: "center", px: "$2", pt: "$2", minW: 0, children: _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode === "dev" ? "dev" : "chat") }) }), _jsxs(XStack, { "data-slot": "workspace-switcher", items: "center", gap: "$2", px: "$2", pt: "$1", pb: "$1", minW: 0, children: [_jsx(Text, { render: _jsx("a", { href: "/", onClick: (e) => {
1233
1222
  if (!navigate || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
1234
1223
  return;
1235
1224
  e.preventDefault();
package/dist/agents.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Host } from '@hanzo/build';
1
2
  export type { Link, Shelf, ShelfRow as Row } from '@hanzo/build';
2
3
  /**
3
4
  * The model that draws an agent's face: Hanzo's own image SKU (hanzo-inc/zen
@@ -7,22 +8,20 @@ export type { Link, Shelf, ShelfRow as Row } from '@hanzo/build';
7
8
  export declare const PAINTER = "zen-image";
8
9
  /** The words every drawn face is asked for after its description. */
9
10
  export declare const MEMOJI = "memoji style 3D emoji portrait, head and shoulders, centered, plain background";
10
- /** A core member, drawn for an org that keeps no row of its own for them. */
11
- export interface House {
12
- id: string;
13
- name: string;
14
- avatar: string;
15
- }
16
- /** The default cast: the core team @hanzo/personas publishes, in the crew's order. */
17
- export declare const CORE: readonly House[];
18
11
  /**
19
- * The org's agents with the default cast filling in any member the org keeps no
20
- * row for, the cast first in its own order, then the rest as listed. With no
21
- * list yet (or none at all) it is the default cast.
12
+ * The default agents an org adds in one press (Customize › Agents, @hanzo/build
13
+ * `hire`): the core team, each as its persona writes it, then QA, Ops and
14
+ * Support. An org starts with none of them and loads none until it adds them.
15
+ */
16
+ export declare const CAST: NonNullable<Host['cast']>;
17
+ /**
18
+ * The org's own agents, the core team's first in the crew's order, then the
19
+ * rest as listed. Nothing stands in for a member the org has not added: an org
20
+ * with no agents has none to show.
22
21
  */
23
22
  export declare function ordered<T extends {
24
23
  name: string;
25
- }>(agents: readonly T[] | null | undefined): (T | House)[];
24
+ }>(agents: readonly T[] | null | undefined): T[];
26
25
  /**
27
26
  * One figure of @hanzo/personas' roster, as assets/personas/index.json lists it
28
27
  * (scripts/team.mjs). `called`, `about` and `based` are its PERSONA.md's
package/dist/agents.js CHANGED
@@ -10,20 +10,25 @@ import { TEAM } from './team.js';
10
10
  export const PAINTER = 'zen-image';
11
11
  /** The words every drawn face is asked for after its description. */
12
12
  export const MEMOJI = 'memoji style 3D emoji portrait, head and shoulders, centered, plain background';
13
- /** The default cast: the core team @hanzo/personas publishes, in the crew's order. */
14
- export const CORE = TEAM.map((one) => ({ id: `house-${one.id}`, name: one.name, avatar: `/agents/${one.id}.png` }));
13
+ /**
14
+ * The default agents an org adds in one press (Customize › Agents, @hanzo/build
15
+ * `hire`): the core team, each as its persona writes it, then QA, Ops and
16
+ * Support. An org starts with none of them and loads none until it adds them.
17
+ */
18
+ export const CAST = [
19
+ ...TEAM.map((one) => ({ name: one.id, description: `${one.name} — ${one.role}`, model: one.model, instructions: one.instructions, avatar: `/agents/${one.id}.png` })),
20
+ { name: 'qa', description: 'QA — Automated Testing & Verification', model: 'enso-auto', instructions: '' },
21
+ { name: 'ops', description: 'Ops — Site Reliability & Cloud Infrastructure', model: 'enso-auto', instructions: '' },
22
+ { name: 'support', description: 'Support — Customer Support & Triage Specialist', model: 'enso-auto', instructions: '' },
23
+ ];
15
24
  const RANK = new Map(TEAM.map((one, i) => [one.id, i]));
16
25
  /**
17
- * The org's agents with the default cast filling in any member the org keeps no
18
- * row for, the cast first in its own order, then the rest as listed. With no
19
- * list yet (or none at all) it is the default cast.
26
+ * The org's own agents, the core team's first in the crew's order, then the
27
+ * rest as listed. Nothing stands in for a member the org has not added: an org
28
+ * with no agents has none to show.
20
29
  */
21
30
  export function ordered(agents) {
22
31
  const list = Array.isArray(agents) ? [...agents] : [];
23
- const names = new Set(list.map((one) => one.name?.toLowerCase()));
24
- for (const one of CORE)
25
- if (!names.has(one.name.toLowerCase()))
26
- list.push(one);
27
32
  const rank = (one) => RANK.get(one.name?.toLowerCase() ?? '') ?? Infinity;
28
33
  return list.sort((a, b) => rank(a) - rank(b));
29
34
  }
package/dist/crew.js CHANGED
@@ -115,6 +115,7 @@ export function useAgentShelf() {
115
115
  loading,
116
116
  error: error ? reach(error) : null,
117
117
  search: 'Search agents',
118
+ empty: 'No agents yet. Add the default team from Manage agents.',
118
119
  create: { label: 'Create agent', onPress: () => make('new') },
119
120
  // Dev's Customize › Agents: a move inside the app where the host reads the
120
121
  // path (hanzo.ai), else Dev on hanzo.ai, as Chat's Create file or site goes.
package/dist/dev.js CHANGED
@@ -35,6 +35,7 @@ import { administers, path, route } from '@hanzo/build';
35
35
  import { bearer, named, org, orgs, pick } from './lib/session.js';
36
36
  import { enter } from './lib/destination.js';
37
37
  import { api } from './lib/api.js';
38
+ import { CAST } from './agents.js';
38
39
  import { app, go, place } from './lib/host.js';
39
40
  import { link } from './lib/tags.js';
40
41
  import { payUrl } from './lib/pay.js';
@@ -117,6 +118,9 @@ export function useDevHost() {
117
118
  path: here,
118
119
  go: (to, how) => (isPlans(to) ? leave('here') : go(app(to), how?.replace)),
119
120
  links: LINKS,
121
+ // The sidebar lists Settings' sections and Customize's tabs (Column.tsx), and Customize › Agents offers the cast.
122
+ sections: true,
123
+ cast: CAST,
120
124
  // An address of the app moves the app, another of this site moves in the
121
125
  // router, and another origin is left in this tab. platform.hanzo.ai is
122
126
  // another product, and a click that names it stays here — everything the
@@ -129,10 +129,18 @@ export declare function follow(was: string | undefined, moved: () => void): () =
129
129
  */
130
130
  export declare const DIRECTORIES: readonly string[];
131
131
  /**
132
- * The organizations this person works in, as the token names them: every org of
133
- * its `orgs` but a directory, which is a home and nobody's tenant. Empty for an
134
- * account that has not made one: the SDK's own list falls back to the `owner`
135
- * claim, which is the application's org and nobody's to work in.
132
+ * The orgs the platform runs under its own names (hanzoai/authz claims.go
133
+ * `platformOrgs`): the brands and the estate's own namespaces. One is a tenant
134
+ * only of the people who run it, its owners and admins, and of a SuperAdmin; a
135
+ * customer who landed in a brand org as a plain member belongs to none of its
136
+ * data, and cloud refuses every request scoped to it.
137
+ */
138
+ export declare const PLATFORM: readonly string[];
139
+ /**
140
+ * The organizations this person works in: the token's `orgs` its claims open
141
+ * (`opened`). Empty for an account that has not made one, or that is only a plain
142
+ * member of a brand org: the SDK's own list falls back to the `owner` claim, which
143
+ * is the application's org and nobody's to work in.
136
144
  */
137
145
  export declare function orgs(): string[];
138
146
  /** The org of the person's own row: the first entry of the token's `orgs`, a directory or not. */
@@ -14,7 +14,7 @@
14
14
  * who was signed in, which reads as a permissions problem rather than a typo.
15
15
  * Two derivations of one name never agree; this is the one derivation.
16
16
  */
17
- import { superAdmin as sudo } from '@hanzo/iam/browser';
17
+ import { superAdmin as sudoOf } from '@hanzo/iam/browser';
18
18
  import { api } from './api.js';
19
19
  import { where } from '../where.js';
20
20
  /** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
@@ -323,13 +323,45 @@ function listed(token) {
323
323
  return out;
324
324
  }
325
325
  /**
326
- * The organizations this person works in, as the token names them: every org of
327
- * its `orgs` but a directory, which is a home and nobody's tenant. Empty for an
328
- * account that has not made one: the SDK's own list falls back to the `owner`
329
- * claim, which is the application's org and nobody's to work in.
326
+ * The orgs the platform runs under its own names (hanzoai/authz claims.go
327
+ * `platformOrgs`): the brands and the estate's own namespaces. One is a tenant
328
+ * only of the people who run it, its owners and admins, and of a SuperAdmin; a
329
+ * customer who landed in a brand org as a plain member belongs to none of its
330
+ * data, and cloud refuses every request scoped to it.
331
+ */
332
+ export const PLATFORM = [
333
+ 'hanzo', 'lux', 'zoo', 'pars', 'platform', 'hanzoai', 'luxfi', 'zooai', 'hanzo-inc', 'hanzo-apps', 'hanzo-community',
334
+ ];
335
+ /**
336
+ * The orgs a token opens to the person it names, home first, each once:
337
+ * hanzoai/authz `Claims.opens`, the rule cloud scopes every request by. A
338
+ * directory opens to no one, `admin` to a SuperAdmin, a platform org to its
339
+ * owners and admins and a SuperAdmin, and any other org to every member.
340
+ */
341
+ function opened(token) {
342
+ const c = claimsOf(token);
343
+ const sudo = sudoOf(c);
344
+ const set = c?.orgs;
345
+ const out = [];
346
+ for (const ref of Array.isArray(set) ? set : []) {
347
+ const m = ref;
348
+ const o = m?.org;
349
+ if (typeof o !== 'string' || !o || out.includes(o) || DIRECTORIES.includes(o))
350
+ continue;
351
+ const runs = typeof m?.role === 'string' && ['owner', 'admin'].includes(m.role.trim().toLowerCase());
352
+ if (o === 'admin' ? sudo : PLATFORM.includes(o) ? sudo || runs : true)
353
+ out.push(o);
354
+ }
355
+ return out;
356
+ }
357
+ /**
358
+ * The organizations this person works in: the token's `orgs` its claims open
359
+ * (`opened`). Empty for an account that has not made one, or that is only a plain
360
+ * member of a brand org: the SDK's own list falls back to the `owner` claim, which
361
+ * is the application's org and nobody's to work in.
330
362
  */
331
363
  export function orgs() {
332
- return listed(bearer()).filter((o) => !DIRECTORIES.includes(o));
364
+ return opened(bearer());
333
365
  }
334
366
  /** The org of the person's own row: the first entry of the token's `orgs`, a directory or not. */
335
367
  export function home() {
@@ -452,7 +484,7 @@ export function choose() {
452
484
  * organization and the step into one.
453
485
  */
454
486
  export function superAdmin() {
455
- return sudo(claims());
487
+ return sudoOf(claims());
456
488
  }
457
489
  /** The organization a SuperAdmin stepped into through IAM, from the token's `assumed` claim, or null. */
458
490
  export function assumed() {
@@ -488,7 +520,7 @@ export async function support(org) {
488
520
  throw new Error(body?.msg || `IAM answered ${res.status}.`);
489
521
  tokens().setItem(ACCESS, next);
490
522
  tokens().setItem(EXPIRES, String(exp * 1000));
491
- pick(org ?? listed(next).find((o) => !DIRECTORIES.includes(o)) ?? '');
523
+ pick(org ?? opened(next)[0] ?? '');
492
524
  window.location.reload();
493
525
  }
494
526
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
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",
@@ -49,7 +49,7 @@
49
49
  "peerDependencies": {
50
50
  "@hanzo/ai": ">=0.6.23",
51
51
  "@hanzo/appearance": ">=0.2.6",
52
- "@hanzo/build": ">=0.2.69",
52
+ "@hanzo/build": ">=0.2.73",
53
53
  "@hanzo/composer": ">=0.2.0",
54
54
  "@hanzo/event": ">=0.3.56",
55
55
  "@hanzo/gui": "^8.3.5",
@@ -67,7 +67,7 @@
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.23",
69
69
  "@hanzo/appearance": "0.2.8",
70
- "@hanzo/build": "0.2.69",
70
+ "@hanzo/build": "0.2.73",
71
71
  "@hanzo/composer": "0.2.0",
72
72
  "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",