@hanzo/rooms 0.1.67 → 0.1.69

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/Account.js CHANGED
@@ -3,28 +3,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * WHO YOU ARE, at the foot of the sidebar: the person, and the person's menu.
5
5
  *
6
- * The plan leads it — @hanzo/build's `Meter`, the block every account menu opens
7
- * with: the plan by its family and its windows as shares, the free allowance on
8
- * Free, the balance only with no plan. Then profile, appearance, usage, billing,
6
+ * The plan leads it — @hanzo/build's `Meter` (Standing.tsx, loaded with the
7
+ * menu), the block every account menu opens with: the plan by its family and its
8
+ * windows as shares, the free allowance on Free, the balance only with no plan. Then profile, appearance, usage, billing,
9
9
  * API keys, security and sign out — the account, and nothing that belongs to the
10
10
  * workspace. Which workspace you stand in is the switcher at the TOP of the
11
11
  * sidebar, and what it may spend is its Billing: a plan never sits beside a
12
12
  * balance here.
13
13
  */
14
- import { useEffect, useMemo, useRef, useState } from 'react';
14
+ import { lazy, Suspense, useEffect, useRef, useState } from 'react';
15
15
  import { Activity, ChevronsUpDown, CreditCard, KeyRound, LogOut, PanelLeftClose, Palette, Shield, UserRound } from 'lucide-react';
16
- import { Meter, useStanding } from '@hanzo/build';
17
16
  import { XStack, YStack, Text, Box } from '@hanzo/ui';
18
17
  import { sheet } from '@hanzo/ui/glass';
19
18
  import { useIam } from '@hanzo/iam/react';
20
19
  import { useSignOut } from './lib/signout.js';
21
20
  import { useAccount } from './lib/account.js';
22
21
  import { Face } from './cast.js';
23
- import { api, iam } from './lib/api.js';
24
- import { bearer, org } from './lib/session.js';
22
+ import { iam } from './lib/api.js';
25
23
  import { link } from './lib/tags.js';
26
24
  import { showSettings } from './open.js';
27
25
  import { site } from './where.js';
26
+ /** The plan at the menu's head, loaded with the menu. */
27
+ const Standing = lazy(() => import('./Standing.js').then((m) => ({ default: m.Standing })));
28
28
  function Row({ icon, onPress, children, }) {
29
29
  return (_jsx(Box, { render: "button", onClick: onPress, width: "100%", px: "$3", py: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [icon, _jsx(Text, { fontSize: "$2", color: "$ink", children: children })] }) }));
30
30
  }
@@ -35,9 +35,6 @@ export function Account({ onToggleCollapse, onProfile, onAppearance, onUsage, on
35
35
  const { user: account } = useAccount();
36
36
  const [open, setOpen] = useState(false);
37
37
  const holder = useRef(null);
38
- const scoped = org();
39
- const target = useMemo(() => ({ api: api(), token: bearer, org: scoped }), [scoped]);
40
- const plan = useStanding(target, open);
41
38
  useEffect(() => {
42
39
  if (!open)
43
40
  return;
@@ -64,5 +61,5 @@ export function Account({ onToggleCollapse, onProfile, onAppearance, onUsage, on
64
61
  // @ts-expect-error — ref is forwarded to the DOM node by the primitive.
65
62
  , {
66
63
  // @ts-expect-error — ref is forwarded to the DOM node by the primitive.
67
- ref: holder, position: "relative", borderTopWidth: 1, borderColor: "$borderColor", children: [open ? (_jsxs(YStack, { role: "menu", "aria-label": "Account", position: "absolute", b: "calc(100% + 6px)", l: "$2", r: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", ...sheet(2), py: "$2", gap: "$1", z: "var(--z-modal)", children: [_jsx(Meter, { read: plan, onPlans: act(() => window.location.assign(site('/pricing'))), onBilling: act(() => showSettings('billing')) }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(UserRound, { size: 15, "aria-hidden": true }), onPress: act(() => (onProfile ? onProfile() : window.location.assign(link(`${iam()}/account`)))), children: "Profile" }), onAppearance ? (_jsx(Row, { icon: _jsx(Palette, { size: 15, "aria-hidden": true }), onPress: act(onAppearance), children: "Appearance" })) : null, onUsage ? (_jsx(Row, { icon: _jsx(Activity, { size: 15, "aria-hidden": true }), onPress: act(onUsage), children: "Usage" })) : null, _jsx(Row, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), onPress: act(() => showSettings('billing')), children: "Billing" }), onKeys ? (_jsx(Row, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), onPress: act(onKeys), children: "API keys" })) : null, _jsx(Row, { icon: _jsx(Shield, { size: 15, "aria-hidden": true }), onPress: act(() => window.location.assign(link(`${iam()}/account/security`))), children: "Security" }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), onPress: act(() => void logout()), children: "Sign out" })] })) : null, _jsxs(XStack, { items: "center", width: "100%", children: [_jsx(Box, { render: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": "Account", flex: 1, minW: 0, px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Face, { src: face, name: called, size: 22 }), _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, children: called }), email && email !== called ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: email })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true })] }) }), onToggleCollapse ? (_jsx(Box, { render: "button", onClick: onToggleCollapse, "aria-label": "Collapse sidebar", p: "$2", mr: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(PanelLeftClose, { size: 16, "aria-hidden": true }) })) : null] })] }));
64
+ ref: holder, position: "relative", borderTopWidth: 1, borderColor: "$borderColor", children: [open ? (_jsxs(YStack, { role: "menu", "aria-label": "Account", position: "absolute", b: "calc(100% + 6px)", l: "$2", r: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", ...sheet(2), py: "$2", gap: "$1", z: "var(--z-modal)", children: [_jsx(Suspense, { fallback: null, children: _jsx(Standing, { onPlans: act(() => window.location.assign(site('/pricing'))), onBilling: act(() => showSettings('billing')) }) }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(UserRound, { size: 15, "aria-hidden": true }), onPress: act(() => (onProfile ? onProfile() : window.location.assign(link(`${iam()}/account`)))), children: "Profile" }), onAppearance ? (_jsx(Row, { icon: _jsx(Palette, { size: 15, "aria-hidden": true }), onPress: act(onAppearance), children: "Appearance" })) : null, onUsage ? (_jsx(Row, { icon: _jsx(Activity, { size: 15, "aria-hidden": true }), onPress: act(onUsage), children: "Usage" })) : null, _jsx(Row, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), onPress: act(() => showSettings('billing')), children: "Billing" }), onKeys ? (_jsx(Row, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), onPress: act(onKeys), children: "API keys" })) : null, _jsx(Row, { icon: _jsx(Shield, { size: 15, "aria-hidden": true }), onPress: act(() => window.location.assign(link(`${iam()}/account/security`))), children: "Security" }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), onPress: act(() => void logout()), children: "Sign out" })] })) : null, _jsxs(XStack, { items: "center", width: "100%", children: [_jsx(Box, { render: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": "Account", flex: 1, minW: 0, px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Face, { src: face, name: called, size: 22 }), _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, children: called }), email && email !== called ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: email })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true })] }) }), onToggleCollapse ? (_jsx(Box, { render: "button", onClick: onToggleCollapse, "aria-label": "Collapse sidebar", p: "$2", mr: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(PanelLeftClose, { size: 16, "aria-hidden": true }) })) : null] })] }));
68
65
  }
package/dist/App.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // The web app as the whole window, in two modes: Chat, a conversation with the
4
- // product's AI, and Dev, @hanzo/build's `<Builder>` without a rail of its own.
3
+ // The web app as the whole window, in three modes: Chat, a conversation with
4
+ // the product's AI; Dev, @hanzo/build's `<Builder>` without a rail of its own;
5
+ // and Studio, @hanzo/studio's `<Studio>` the same way, its places (`nav`) in the
6
+ // sidebar where Dev's stand for Dev.
5
7
  //
6
8
  // OUTER the rail (Rail.tsx) and the sidebar beside it (Column.tsx), on the
7
9
  // window's floor, with the window's bar above them where the host has
@@ -14,11 +16,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
14
16
  // search, New, Dev's places and the reader's Recents. The address says the
15
17
  // mode, as a path (lib/host `talk`, `app`, `place`): `/` and `/chat` are Chat on
16
18
  // a new conversation, `/chat/<id>` one of the reader's, and a path under `/dev`
17
- // is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project. Moving
19
+ // is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project; a path
20
+ // under `/studio` is Studio, its own address after it (`c/<id>`, `library`). Moving
18
21
  // between the modes is a history entry (`go`), never a load, so a host serves
19
22
  // this one component for every path under each.
20
23
  //
21
- // Every host draws this: hanzo.ai at `/`, `/chat` and `/dev`, and the desktop
24
+ // Every host draws this: hanzo.ai at `/`, `/chat`, `/dev` and `/studio`, and the desktop
22
25
  // app as its main window. The host mounts IamProvider and `<Rooms>` with its
23
26
  // router and the path, and draws this only for a signed-in reader; what a
24
27
  // stranger sees is the host's.
@@ -46,13 +49,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
46
49
  // EVERY MOVE PUTS THE DRAWER AWAY, the one to where the reader already is
47
50
  // included.
48
51
  //
52
+ // STUDIO LOADS WITH ITS MODE. Its module (and three.js behind a 3D tab) is
53
+ // fetched the first time the reader stands in Studio; its places are a few
54
+ // lines and come with the app. Another organization opens it at its New, so
55
+ // nothing it read under the last one stays.
56
+ //
49
57
  // THE PLANS' OWN ADDRESS LEAVES for the pay site in its place (dev.tsx `leave`),
50
58
  // so the builder's own plans are never drawn.
51
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
59
+ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
52
60
  import { ArrowLeft, ArrowRight, Menu } from 'lucide-react';
53
61
  import { AiProvider } from '@hanzo/ai/react';
54
62
  import { Text, XStack, YStack } from '@hanzo/gui';
55
63
  import { Builder, HostProvider, path as written, route } from '@hanzo/build';
64
+ import { nav as places } from '@hanzo/studio';
56
65
  import { SidebarIconButton } from '@hanzo/ui/chat';
57
66
  import { useAi } from './lib/ai.js';
58
67
  import { Chat } from './Chat.js';
@@ -66,8 +75,10 @@ import { empty, openThread, useOpen } from './open.js';
66
75
  import { Paned } from './Shell.js';
67
76
  import { floor, pane } from './ground.js';
68
77
  import { pick } from './lib/session.js';
69
- import { app, go, place, talk } from './lib/host.js';
78
+ import { app, go, place, studio, talk } from './lib/host.js';
70
79
  import { wordmark } from './where.js';
80
+ /** Studio, fetched the first time the reader stands in it. */
81
+ const Studio = lazy(() => import('@hanzo/studio').then((m) => ({ default: m.Studio })));
71
82
  /** `/` is Chat on a new conversation. */
72
83
  const HOME = { mode: 'chat', thread: null };
73
84
  /** The bar's height, px: the reference window's title bar. */
@@ -85,7 +96,7 @@ const INSET = {
85
96
  left: 'max(var(--pane-gap), var(--safe-left))',
86
97
  };
87
98
  /** Where each mode was last, so the rail goes back to it. */
88
- const last = { chat: '', dev: '' };
99
+ const last = { chat: '', dev: '', studio: '' };
89
100
  /** Whether a key press is the reader typing, where the app's keys are the field's. */
90
101
  const typing = (e) => {
91
102
  const el = e.target;
@@ -119,6 +130,22 @@ function Window({ bar }) {
119
130
  const at = place(here) ?? HOME;
120
131
  const mode = at.mode;
121
132
  const where = at.mode === 'dev' ? at.at : null;
133
+ // Studio's host: Dev's, at Studio's address, its moves under /studio, and
134
+ // another organization opening Studio's New in it.
135
+ const made = at.mode === 'studio' ? at.at : '';
136
+ const maker = useMemo(() => ({
137
+ ...host,
138
+ name: 'Hanzo Studio',
139
+ path: made,
140
+ go: (to, how) => {
141
+ shut();
142
+ go(studio(to), how?.replace);
143
+ },
144
+ chooseOrg: (next) => {
145
+ pick(next);
146
+ window.location.assign(studio(''));
147
+ },
148
+ }), [host, made, shut]);
122
149
  // A run floats two panes of its own, so the pane it stands in is not cut.
123
150
  const run = where !== null && route(where).kind === 'run';
124
151
  const asked = at.mode === 'chat' ? at.thread : null;
@@ -202,11 +229,13 @@ function Window({ bar }) {
202
229
  }, [bar, router, move]);
203
230
  const toChat = () => move(last.chat || talk());
204
231
  const toDev = () => move(last.dev || app(''));
232
+ const toStudio = () => move(last.studio || studio(''));
205
233
  // Another organization: picked, and the app opened in it at the mode's New.
206
234
  const org = (o) => {
207
235
  pick(o);
208
- window.location.assign(mode === 'dev' ? app('') : talk());
236
+ window.location.assign(mode === 'dev' ? app('') : mode === 'studio' ? studio('') : talk());
209
237
  };
238
+ const sidebar = mode === 'studio' ? places(maker, (p) => maker.go(p)) : undefined;
210
239
  const flip = () => setClosed(!closed);
211
- return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(YStack, { "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", 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, 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), seek: seek }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0 }, children: [bar ? null : (_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] })), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : (_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, seek: seek, drawer: true, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }) })] })) : null] }) }) }));
240
+ return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(YStack, { "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", 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), seek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0 }, children: [bar ? null : (_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] })), _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, seek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }) })] })) : null] }) }) }));
212
241
  }
package/dist/Column.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { type Host } from '@hanzo/build';
2
+ import { type RailLink } from '@hanzo/ui/chat';
2
3
  import { type ReactNode } from 'react';
3
- import { type Mode } from './recent.js';
4
- export declare function Column({ host, mode, thread, go, onShut, seek, drawer, foot, }: {
4
+ import { type Place } from './lib/host.js';
5
+ export declare function Column({ host, mode, thread, go, onShut, seek, drawer, foot, places, }: {
5
6
  host: Host;
6
- mode: Mode;
7
+ mode: Place['mode'];
7
8
  /** The Chat conversation the pane holds, or null. */
8
9
  thread: string | null;
9
10
  /** Moves the app to an address of this page. */
@@ -16,4 +17,10 @@ export declare function Column({ host, mode, thread, go, onShut, seek, drawer, f
16
17
  drawer?: boolean;
17
18
  /** What stands at its foot: the drawer's rail, laid flat. */
18
19
  foot?: ReactNode;
20
+ /** The mode's places, where they are not Dev's (Studio's `nav`). */
21
+ places?: {
22
+ id: string;
23
+ label: string;
24
+ links: RailLink[];
25
+ }[];
19
26
  }): import("react").JSX.Element;
package/dist/Column.js CHANGED
@@ -89,7 +89,7 @@ function Activity({ host }) {
89
89
  ];
90
90
  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 }));
91
91
  }
92
- export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = false, foot, }) {
92
+ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = false, foot, places, }) {
93
93
  const [words, setWords] = useState('');
94
94
  // The search field shows once asked for, and while it holds words.
95
95
  const [seeking, setSeeking] = useState(false);
@@ -98,7 +98,7 @@ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = fals
98
98
  const uid = useId();
99
99
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
100
100
  const dev = (p) => host.go(p);
101
- const groups = nav(host, dev);
101
+ const groups = places ?? nav(host, dev);
102
102
  const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
103
103
  // Chat's New; Dev's is the first of its places.
104
104
  const create = () => {
@@ -119,13 +119,13 @@ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = fals
119
119
  }, [seek]);
120
120
  return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
121
121
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
122
- 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: mode === 'chat' ? 'Search chats' : 'Search runs', onPress: seeking ? done : ask, "aria-expanded": seeking || !!words, ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) })] }), seeking || words ? (_jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", pl: "$3", pr: "$1", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, mb: "$1.5", children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { ref: field, value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
122
+ 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 }), mode === 'studio' ? null : (_jsx(SidebarIconButton, { label: mode === 'chat' ? 'Search chats' : 'Search runs', onPress: seeking ? done : ask, "aria-expanded": seeking || !!words, ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) }))] }), seeking || words ? (_jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", pl: "$3", pr: "$1", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, mb: "$1.5", children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { ref: field, value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
123
123
  if (e.key === 'Escape')
124
124
  done();
125
125
  }, onBlur: () => {
126
126
  if (!words)
127
127
  setSeeking(false);
128
- }, placeholder: mode === 'chat' ? 'Search chats' : 'Search runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$3" }), words ? (_jsx(SidebarIconButton, { label: "Clear search", onPress: done, width: 28, height: 28, rounded: 999, children: _jsx(X, { size: 14, "aria-hidden": true }) })) : null] })) : null, 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))) })), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, 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",
128
+ }, placeholder: mode === 'chat' ? 'Search chats' : 'Search runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$3" }), words ? (_jsx(SidebarIconButton, { label: "Clear search", onPress: done, width: 28, height: 28, rounded: 999, children: _jsx(X, { size: 14, "aria-hidden": true }) })) : null] })) : null, 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, words: words, 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",
129
129
  // In the gutter between the sidebar and the panel, not over the sidebar's own rows.
130
130
  r: GUTTER_END, width: "var(--pane-gap)" }))] }));
131
131
  }
package/dist/Rail.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { type Host } from '@hanzo/build';
3
- import type { Mode } from './recent.js';
3
+ import { type Place } from './lib/host.js';
4
4
  /** The rail's width, px. */
5
5
  export declare const RAIL = 48;
6
6
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
@@ -18,9 +18,9 @@ export declare function Toggle({ open, onPress, label }: {
18
18
  onPress: () => void;
19
19
  label?: string;
20
20
  }): import("react").JSX.Element;
21
- export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, go, onOrg, }: {
21
+ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, onStudio, go, onOrg, }: {
22
22
  host: Host;
23
- mode: Mode;
23
+ mode: Place['mode'];
24
24
  /** Dev's builder address, or null in Chat. */
25
25
  at: string | null;
26
26
  /** The sidebar beside the rail is open. */
@@ -29,9 +29,10 @@ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev
29
29
  toggle?: () => void;
30
30
  /** Laid along the foot of the phone's drawer rather than down the window's edge. */
31
31
  flat?: boolean;
32
- /** To Chat and to Dev, each where the reader left it. */
32
+ /** To Chat, Dev and Studio, each where the reader left it. */
33
33
  onChat: () => void;
34
34
  onDev: () => void;
35
+ onStudio: () => void;
35
36
  /** Moves the app to an address of this page. */
36
37
  go: (address: string) => void;
37
38
  /** Work in another organization. */
package/dist/Rail.js CHANGED
@@ -5,8 +5,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  // menu of the rest, and the account at the foot. It is how the app moves
6
6
  // between its products; there is no switch anywhere else.
7
7
  //
8
- // Studio is its own page, hanzo.ai/studio, opened in this tab: the app draws it
9
- // there for a signed-in reader.
8
+ // Studio is the app's third mode (@hanzo/studio at /studio), each of the three
9
+ // opened where the reader left it.
10
10
  //
11
11
  // Library, Scheduled tasks and Customize are Dev's own places (@hanzo/build's
12
12
  // Artifacts, Automations and Customize), raised to the rail; each is lit while
@@ -27,8 +27,7 @@ import { XStack, YStack } from '@hanzo/gui';
27
27
  import { route } from '@hanzo/build';
28
28
  import { DropdownMenu } from '@hanzo/ui';
29
29
  import { Me } from './Me.js';
30
- import { app, STUDIO, TABS, WORKSPACE } from './lib/host.js';
31
- import { site } from './where.js';
30
+ import { app, TABS, WORKSPACE } from './lib/host.js';
32
31
  /** The rail's width, px. */
33
32
  export const RAIL = 48;
34
33
  /** A mark, px: a 36px square on a 44px pitch. */
@@ -72,7 +71,7 @@ export function Mark({ label, on = false, onPress, children, ...rest }) {
72
71
  export function Toggle({ open, onPress, label }) {
73
72
  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 }) }));
74
73
  }
75
- export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, go, onOrg, }) {
74
+ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, onStudio, go, onOrg, }) {
76
75
  const here = mode === 'dev' ? raised(at) : null;
77
76
  const to = (p) => () => go(app(p));
78
77
  // Everything else the app reaches, behind More: on a phone the raised places first, then Dev's other places, then the products on hosts of their own.
@@ -95,5 +94,5 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
95
94
  { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
96
95
  ];
97
96
  const Stack = flat ? XStack : YStack;
98
- 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", onPress: () => window.location.assign(site(STUDIO)), "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 })] }));
97
+ 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 })] }));
99
98
  }
@@ -12,6 +12,8 @@ export type Row = {
12
12
  /** One sentence, for a pane we have not built a screen for yet. */
13
13
  note?: string;
14
14
  };
15
+ /** Every row, flat — what the pane looks a chosen id up in. */
16
+ export declare const ROWS: Row[];
15
17
  /** The row Settings opens on when nothing has asked for another. */
16
18
  export declare const FIRST: string;
17
19
  /**
@@ -34,13 +36,3 @@ export declare function SettingsList({ active, filter, onPick, onBack, }: {
34
36
  /** Back to the rooms. */
35
37
  onBack: () => void;
36
38
  }): import("react").JSX.Element;
37
- /**
38
- * The room, while a settings row is open.
39
- *
40
- * It scrolls itself, the way the column beside a room does: `main` gives its
41
- * child a floor and no overflow, so a pane that did not would push the whole
42
- * shell down the page.
43
- */
44
- export declare function SettingsPane({ id }: {
45
- id: string;
46
- }): import("react").JSX.Element;
package/dist/Settings.js CHANGED
@@ -3,8 +3,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  // Settings: the sidebar's second level, and the pane it opens in the room.
4
4
  //
5
5
  // TWO PARTS, ONE TAXONOMY. `SettingsList` is the rail's list — the shell swaps
6
- // its own rows for these and back. `SettingsPane` is what fills the room while
7
- // a row is chosen. Both read `GROUPS`, so the list can only offer a row the
6
+ // its own rows for these and back. `SettingsPane` (SettingsPane.tsx) is what
7
+ // fills the room while a row is chosen, loaded only then: it carries the plan,
8
+ // the usage panel, the model picker and the catalog, none of which a room's
9
+ // first screen draws. Both read `GROUPS`, so the list can only offer a row the
8
10
  // pane can draw.
9
11
  //
10
12
  // Which row is open is the shell's `showSettings` value and nothing here: a
@@ -22,31 +24,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
22
24
  // The panes that are not yet built say so in one sentence rather than pretending
23
25
  // to a screen. An honest empty pane is a smaller lie than an invented one.
24
26
  import { app, dev } from './lib/host.js';
25
- import { useMemo, useState } from 'react';
26
- import { ArrowLeft, ArrowUpRight, ExternalLink } from 'lucide-react';
27
- import { Box, Button, Separator, Text, XStack, YStack, } from '@hanzo/ui';
27
+ import { ArrowLeft, ArrowUpRight } from 'lucide-react';
28
+ import { Text } from '@hanzo/ui';
28
29
  import { SidebarItem, SidebarSection } from '@hanzo/ui/chat';
29
- import { tap } from './lib/tap.js';
30
- import { useIam, useIamIdentity, useOrganizations } from '@hanzo/iam/react';
31
- import { useSignOut } from './lib/signout.js';
32
- import { useModels } from '@hanzo/ai/react';
33
- import { ModelPicker } from '@hanzo/ui/models';
34
- import { parseModels } from '@hanzo/ui/models/catalog';
35
- import { UsagePanel } from '@hanzo/usage/panel';
36
- import { administers, Credits, label, Plan, Title, useChoice } from '@hanzo/build';
37
- import { Catalog } from './Directory.js';
38
- import { payPage } from './lib/pay.js';
39
- import { api } from './lib/api.js';
40
- import { renewal, useSubscription, useTier } from './lib/tier.js';
41
- import { spendShown, useLimits } from './lib/limits.js';
42
- import { showSettings } from './open.js';
43
- import { Look } from './look.js';
44
- import { ProviderMark } from './ProviderMark.js';
45
- import { bearer, org, pick } from './lib/session.js';
46
30
  import { site } from './where.js';
47
- /** A choice among a few: a hairline edge and a corner. The fill and the edge's
48
- * ink are the call site's, because they say which one is chosen. */
49
- const CHOICE = { rounded: 8, borderWidth: 1, borderStyle: 'solid', cursor: 'pointer' };
50
31
  const GROUPS = [
51
32
  {
52
33
  label: 'Settings',
@@ -109,7 +90,7 @@ const GROUPS = [
109
90
  },
110
91
  ];
111
92
  /** Every row, flat — what the pane looks a chosen id up in. */
112
- const ROWS = GROUPS.flatMap((g) => g.rows);
93
+ export const ROWS = GROUPS.flatMap((g) => g.rows);
113
94
  /** The row Settings opens on when nothing has asked for another. */
114
95
  export const FIRST = ROWS[0].id;
115
96
  /**
@@ -133,196 +114,3 @@ export function SettingsList({ active, filter, onPick, onBack, }) {
133
114
  // pressed.
134
115
  icon: r.href ? _jsx(ArrowUpRight, { size: 13, "aria-hidden": true }) : undefined, active: r.id === active, "aria-current": r.id === active ? 'true' : undefined, onPress: () => onPick(r), children: r.label }, r.id))) }, g.label)))] }));
135
116
  }
136
- /**
137
- * The room, while a settings row is open.
138
- *
139
- * It scrolls itself, the way the column beside a room does: `main` gives its
140
- * child a floor and no overflow, so a pane that did not would push the whole
141
- * shell down the page.
142
- */
143
- export function SettingsPane({ id }) {
144
- const row = ROWS.find((r) => r.id === id);
145
- return (_jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", children: _jsxs(YStack, { p: "$5", gap: "$4", maxW: 860, children: [_jsx(Text, { fontSize: "$6", fontWeight: "600", color: "$ink", children: row?.label ?? 'Settings' }), _jsx(Pane, { id: id, note: row?.note })] }) }));
146
- }
147
- /** One pane. */
148
- function Pane({ id, note }) {
149
- if (id === 'apps' || id === 'channels')
150
- return _jsx(Catalog, { tab: id });
151
- if (id === 'general')
152
- return _jsx(Look, {});
153
- if (id === 'account')
154
- return _jsx(Account, {});
155
- if (id === 'billing')
156
- return _jsx(Billing, {});
157
- if (id === 'usage')
158
- return _jsx(Usage, {});
159
- if (id === 'model')
160
- return _jsx(Model, {});
161
- if (id === 'models')
162
- return _jsx(Served, {});
163
- if (id === 'agent')
164
- return _jsx(AgentSettings, {});
165
- if (id === 'capabilities')
166
- return _jsx(Capabilities, {});
167
- if (id === 'memory')
168
- return _jsx(MemorySettings, {});
169
- return (_jsx(Text, { fontSize: "$3", color: "$soft", children: note ?? 'Nothing to configure here yet.' }));
170
- }
171
- /**
172
- * One default in one place; agents may still bring their own model. A premium
173
- * model is never a default: it is picked per conversation, in the composer, so
174
- * this picker offers the rest of the catalog.
175
- */
176
- function Model() {
177
- const { models: served } = useModels();
178
- const models = useMemo(() => parseModels(served).filter((m) => m.class !== 'premium'), [served]);
179
- // The one choice Chat and Dev share (@hanzo/build): changed here, changed in both composers.
180
- const [choice, choose] = useChoice();
181
- return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats and new runs. An agent with its own model keeps that choice. Premium models are picked per conversation in the composer." }), _jsx(XStack, { self: "flex-start", children: _jsx(ModelPicker, { models: models, scope: "chat", value: choice.model, onChange: (id) => choose({ model: id }), size: "sm" }) })] }));
182
- }
183
- /**
184
- * WHAT THE GATEWAY SERVES, read from the same `/v1/models` the rooms read.
185
- *
186
- * Counted, never typed: the catalog moves without this file, so a number written
187
- * here is wrong by the next deploy. An empty answer says the account reaches
188
- * nothing rather than drawing a zero, and the mark comes from the model's own
189
- * id so a family wears the logo of the lab that makes it.
190
- */
191
- function Served() {
192
- const { models, loading, error } = useModels();
193
- if (loading) {
194
- return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading the catalog." });
195
- }
196
- if (error) {
197
- return _jsxs(Text, { fontSize: "$3", color: "$soft", children: ["The catalog did not answer: ", error.message] });
198
- }
199
- if (models.length === 0) {
200
- return (_jsx(Text, { fontSize: "$3", color: "$soft", children: "No model answers on this account. Sign in to reach the ones your org opens." }));
201
- }
202
- return (_jsxs(YStack, { gap: "$3", children: [_jsxs(Text, { fontSize: "$3", color: "$soft", children: [models.length, " models answer at /v1/models on this account."] }), _jsx(YStack, { children: models.map((m) => (_jsxs(XStack, { items: "center", gap: "$2.5", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(ProviderMark, { model: m.id, org: m.owned_by, size: 16, ink: true }), _jsx(Text, { fontSize: "$2", color: "$ink", flex: 1, children: m.id }), m.owned_by ? _jsx(Text, { fontSize: "$1", color: "$soft", children: m.owned_by }) : null] }, m.id))) }), _jsx(XStack, { children: _jsx(Box, { render: _jsx("a", { href: site('/models') }), ...tap, children: _jsxs(XStack, { items: "center", gap: "$2", py: "$2", children: [_jsx(Text, { fontSize: "$3", color: "$ink", children: "Browse the model catalog" }), _jsx(ArrowUpRight, { size: 13, "aria-hidden": true })] }) }) })] }));
203
- }
204
- /** Who is signed in, and where they can act. */
205
- function Account() {
206
- const identity = useIamIdentity();
207
- const { isAuthenticated } = useIam();
208
- const logout = useSignOut();
209
- const { organizations, currentOrgId, currentRole, switchOrg } = useOrganizations();
210
- if (!isAuthenticated || !identity) {
211
- return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your account." });
212
- }
213
- return (_jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(YStack, { width: 44, height: 44, rounded: 22, bg: "$borderColor", items: "center", justify: "center", children: _jsx(Text, { fontSize: "$3", color: "$ink", children: identity.initials }) }), _jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$4", color: "$ink", children: identity.name }), identity.email ? _jsx(Text, { fontSize: "$2", color: "$soft", children: identity.email }) : null] })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Organizations" }), organizations.length === 0 ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "You are not a member of an organization." })) : (organizations.map((o) => {
214
- const here = o.name === currentOrgId;
215
- return (_jsx(Box, { render: _jsx("button", { type: "button" }), onClick: () => { pick(o.name); switchOrg(o.name); }, ...tap, children: _jsxs(XStack, { items: "center", justify: "space-between", py: "$2", width: "100%", gap: "$2", children: [_jsx(Text, { fontSize: "$3", color: here ? '$ink' : '$soft', children: o.displayName || o.name }), here && currentRole ? _jsx(Text, { fontSize: "$2", color: "$soft", children: currentRole }) : null] }) }, o.name));
216
- }))] }), _jsx(Separator, {}), _jsx(XStack, { children: _jsx(Button, { size: "sm", onClick: () => logout(), children: "Sign out" }) })] }));
217
- }
218
- /** Where the plan's and billing's reads go: this browser's bearer, in its organization. */
219
- function useTarget() {
220
- const scoped = org();
221
- return useMemo(() => ({ api: api(), token: bearer, org: scoped }), [scoped]);
222
- }
223
- /**
224
- * The plan and its term, then the credits — @hanzo/build's `Credits`, the one
225
- * view every Billing page draws: the balance, grants, auto-reload, this month's
226
- * spend and its limit, said to be separate from the plan's usage. Money moves on
227
- * the payment host, so adding funds and a card both open it.
228
- */
229
- function Billing() {
230
- const { isAuthenticated } = useIam();
231
- const t = useTarget();
232
- const { tier } = useTier(isAuthenticated, org());
233
- const sub = useSubscription(isAuthenticated, org());
234
- if (!isAuthenticated)
235
- return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see billing." });
236
- // WHAT THE CUSTOMER BOUGHT, by its family: `Max` with `20x` beside it.
237
- const plan = tier ? label(tier.plan, tier.tier.name) ?? { name: 'Free', tag: '' } : null;
238
- return (_jsxs(YStack, { gap: "$5", children: [plan ? (_jsxs(YStack, { gap: "$1", "data-slot": "billing-plan", children: [_jsx(Title, { label: plan, size: "$5" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: sub ? renewal(sub) : tier?.plan ? 'Your plan.' : 'No paid plan. Usage is paid from credits.' })] })) : (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading your plan\u2026" })), _jsx(Credits, { target: t, admin: administers(bearer(), org()), add: payPage() }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Payment" }), _jsx(Text, { fontSize: "$3", color: "$soft", children: "A card is entered on the payment host and nowhere else, so this is the one control here that opens another surface." }), _jsx(XStack, { children: _jsx(Box, { render: _jsx("a", { href: payPage(), target: "_blank", rel: "noreferrer" }), ...tap, children: _jsxs(XStack, { items: "center", gap: "$2", py: "$2", children: [_jsx(Text, { fontSize: "$3", color: "$ink", children: "Manage plan, payment method and invoices" }), _jsx(ExternalLink, { size: 13, "aria-hidden": true })] }) }) })] })] }));
239
- }
240
- /**
241
- * Usage: @hanzo/build's `Plan`, the one view every Usage page draws — the plan's
242
- * windows and each class of model's share, as shares; the free allowance on
243
- * Free; the balance with no plan — and the choice of paying from credits once
244
- * the allowance runs out. A reader with no paid plan also sees what was spent,
245
- * over time (@hanzo/usage), because money is their meter.
246
- */
247
- function Usage() {
248
- const { accessToken, isAuthenticated } = useIam();
249
- const t = useTarget();
250
- const read = useLimits(isAuthenticated, org());
251
- if (!isAuthenticated || !accessToken) {
252
- return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
253
- }
254
- const plan = _jsx(Plan, { target: t, onPlans: () => window.location.assign(site('/pricing')), onBilling: () => showSettings('billing') });
255
- if (!spendShown(read))
256
- return plan;
257
- // The address is read HERE rather than at import: the panel sends the bearer
258
- // with every read, and a credentialed read to an absolute address from a page
259
- // the gateway does not admit dies in preflight — which drew this panel as an
260
- // account that had spent nothing.
261
- return (_jsxs(YStack, { gap: "$6", children: [plan, _jsx(UsagePanel, { baseUrl: api(), token: accessToken, upgrade: payPage(), title: "Spend" })] }));
262
- }
263
- const HISTORICAL_PERSONAS = [
264
- { id: 'architect', name: 'Software Architect', role: 'Full-stack & systems design' },
265
- { id: 'satoshi', name: 'Satoshi Nakamoto', role: 'Distributed cryptography & consensus' },
266
- { id: 'turing', name: 'Alan Turing', role: 'Algorithmic logic & formal verification' },
267
- { id: 'ada', name: 'Ada Lovelace', role: 'Computational precision & analysis' },
268
- { id: 'davinci', name: 'Leonardo da Vinci', role: 'Polymathic engineering & design' },
269
- ];
270
- function AgentSettings() {
271
- const [instructions, setInstructions] = useState(() => {
272
- if (typeof window === 'undefined')
273
- return '';
274
- return localStorage.getItem('hanzo.agent.instructions') || 'You are an autonomous AI software engineer at Hanzo. Think step-by-step and write robust code.';
275
- });
276
- const [persona, setPersona] = useState(() => {
277
- if (typeof window === 'undefined')
278
- return 'architect';
279
- return localStorage.getItem('hanzo.agent.persona') || 'architect';
280
- });
281
- const [autonomy, setAutonomy] = useState(() => {
282
- if (typeof window === 'undefined')
283
- return 'autonomous';
284
- return localStorage.getItem('hanzo.agent.autonomy') || 'autonomous';
285
- });
286
- const [saved, setSaved] = useState(false);
287
- const save = () => {
288
- localStorage.setItem('hanzo.agent.instructions', instructions);
289
- localStorage.setItem('hanzo.agent.persona', persona);
290
- localStorage.setItem('hanzo.agent.autonomy', autonomy);
291
- setSaved(true);
292
- setTimeout(() => setSaved(false), 2000);
293
- };
294
- return (_jsxs(YStack, { gap: "$4", maxW: 480, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Nothing is bound to this surface yet \u2014 when you bind a personal agent, its persona, tools and memory live here rather than in a shared team." }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "Persona & Historical Character" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Pick an intellectual archetype for agent reasoning and tone." }), _jsx(XStack, { flexWrap: "wrap", gap: "$2", pt: "$1", children: HISTORICAL_PERSONAS.map((p) => {
295
- const active = persona === p.id;
296
- return (_jsx(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => setPersona(p.id), ...CHOICE, bg: active ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: active ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', py: 6, px: 12, children: _jsx(Text, { fontSize: "$2", fontWeight: active ? '600' : '400', color: active ? '$ink' : '$soft', children: p.name }) }, p.id));
297
- }) })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "System Prompt Instructions" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Custom system directions prepended to all coding and research runs." }), _jsx(Text, { render: _jsx("textarea", { value: instructions, onChange: (e) => setInstructions(e.target.value), rows: 4 }), width: "100%", bg: "var(--panel, color-mix(in srgb, var(--pure-black) 30%, transparent))", color: "inherit", borderWidth: 1, borderColor: "var(--borderColor, var(--white-15))", rounded: 8, p: 10, fontSize: "$2", fontFamily: "$mono" })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "Autonomy & Permissions" }), _jsxs(XStack, { gap: "$2", children: [_jsxs(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => setAutonomy('autonomous'), ...CHOICE, flex: 1, p: 10, bg: autonomy === 'autonomous' ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: autonomy === 'autonomous' ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Autonomous" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Executes tools & writes code without interrupting." })] }), _jsxs(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => setAutonomy('supervised'), ...CHOICE, flex: 1, p: 10, bg: autonomy === 'supervised' ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: autonomy === 'supervised' ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Supervised" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Requests confirmation before file writes and commands." })] })] })] }), _jsx(XStack, { self: "flex-start", pt: "$2", children: _jsx(Button, { size: "sm", onClick: save, children: saved ? 'Saved' : 'Save agent preferences' }) })] }));
298
- }
299
- /**
300
- * WHAT A RUN MAY REACH FOR — the four capabilities, described and not measured.
301
- *
302
- * Each row used to carry a badge reading "Enabled", and the MCP row read
303
- * "Active (:8080/mcp)". Nothing asked. No call stands behind any of the four,
304
- * so the badges said "on" for a reader whose run had none of it, and the port
305
- * was one developer's machine printed on everybody's settings pane. A green
306
- * light nobody wired is worse than no light: it is read as a check that passed.
307
- *
308
- * These are facts about the PRODUCT — what a run is allowed to do — which is
309
- * what this pane is for. Whether a given run got them is a fact about that run,
310
- * and the run is where it belongs.
311
- */
312
- function Capabilities() {
313
- return (_jsxs(YStack, { gap: "$3", maxW: 480, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "What models and agent swarms are granted access to during tasks." }), _jsx(YStack, { gap: "$2", pt: "$1", children: [
314
- { title: 'Filesystem (Read / Write)', desc: 'Create, modify, and delete workspace files in the target repository.' },
315
- { title: 'Shell & Terminal Exec', desc: 'Execute build, test, and shell commands in isolated sandbox VMs.' },
316
- { title: 'Live Web & Search', desc: 'Query internet search APIs and fetch web documentation.' },
317
- { title: 'MCP Protocol Server', desc: 'Expose local and remote tools via Model Context Protocol at /mcp.' },
318
- ].map((cap) => (_jsxs(YStack, { gap: "$0.5", p: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: cap.title }), _jsx(Text, { fontSize: "$2", color: "$soft", children: cap.desc })] }, cap.title))) })] }));
319
- }
320
- function MemorySettings() {
321
- const [cleared, setCleared] = useState(false);
322
- const clearMemory = () => {
323
- localStorage.removeItem('hanzo.chat.history');
324
- setCleared(true);
325
- setTimeout(() => setCleared(false), 2000);
326
- };
327
- return (_jsxs(YStack, { gap: "$3", maxW: 480, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Semantic memory and conversation recall across sessions." }), _jsxs(YStack, { gap: "$0.5", p: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "Semantic Vector Index" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Automatically indexes past workspace sessions into vector memory." })] }), _jsx(XStack, { self: "flex-start", pt: "$2", children: _jsx(Button, { size: "sm", variant: "outline", onClick: clearMemory, children: cleared ? 'Memory cleared' : 'Clear local memory cache' }) })] }));
328
- }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The room, while a settings row is open.
3
+ *
4
+ * It scrolls itself, the way the column beside a room does: `main` gives its
5
+ * child a floor and no overflow, so a pane that did not would push the whole
6
+ * shell down the page.
7
+ */
8
+ export declare function SettingsPane({ id }: {
9
+ id: string;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,222 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The room while a settings row is open: every section's screen (Settings.tsx
4
+ // holds the rows). Loaded when a row is chosen, never with the room.
5
+ import { useMemo, useState } from 'react';
6
+ import { ArrowUpRight, ExternalLink } from 'lucide-react';
7
+ import { Box, Button, Separator, Text, XStack, YStack, } from '@hanzo/ui';
8
+ import { tap } from './lib/tap.js';
9
+ import { useIam, useIamIdentity, useOrganizations } from '@hanzo/iam/react';
10
+ import { useSignOut } from './lib/signout.js';
11
+ import { useModels } from '@hanzo/ai/react';
12
+ import { ModelPicker } from '@hanzo/ui/models';
13
+ import { parseModels } from '@hanzo/ui/models/catalog';
14
+ import { UsagePanel } from '@hanzo/usage/panel';
15
+ import { administers, Credits, label, Plan, Title, useChoice } from '@hanzo/build';
16
+ import { Catalog } from './Directory.js';
17
+ import { payPage } from './lib/pay.js';
18
+ import { api } from './lib/api.js';
19
+ import { renewal, useSubscription, useTier } from './lib/tier.js';
20
+ import { spendShown, useLimits } from './lib/limits.js';
21
+ import { showSettings } from './open.js';
22
+ import { Look } from './look.js';
23
+ import { ProviderMark } from './ProviderMark.js';
24
+ import { bearer, org, pick } from './lib/session.js';
25
+ import { site } from './where.js';
26
+ import { ROWS } from './Settings.js';
27
+ /** A choice among a few: a hairline edge and a corner. The fill and the edge's
28
+ * ink are the call site's, because they say which one is chosen. */
29
+ const CHOICE = { rounded: 8, borderWidth: 1, borderStyle: 'solid', cursor: 'pointer' };
30
+ /**
31
+ * The room, while a settings row is open.
32
+ *
33
+ * It scrolls itself, the way the column beside a room does: `main` gives its
34
+ * child a floor and no overflow, so a pane that did not would push the whole
35
+ * shell down the page.
36
+ */
37
+ export function SettingsPane({ id }) {
38
+ const row = ROWS.find((r) => r.id === id);
39
+ return (_jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", children: _jsxs(YStack, { p: "$5", gap: "$4", maxW: 860, children: [_jsx(Text, { fontSize: "$6", fontWeight: "600", color: "$ink", children: row?.label ?? 'Settings' }), _jsx(Pane, { id: id, note: row?.note })] }) }));
40
+ }
41
+ /** One pane. */
42
+ function Pane({ id, note }) {
43
+ if (id === 'apps' || id === 'channels')
44
+ return _jsx(Catalog, { tab: id });
45
+ if (id === 'general')
46
+ return _jsx(Look, {});
47
+ if (id === 'account')
48
+ return _jsx(Account, {});
49
+ if (id === 'billing')
50
+ return _jsx(Billing, {});
51
+ if (id === 'usage')
52
+ return _jsx(Usage, {});
53
+ if (id === 'model')
54
+ return _jsx(Model, {});
55
+ if (id === 'models')
56
+ return _jsx(Served, {});
57
+ if (id === 'agent')
58
+ return _jsx(AgentSettings, {});
59
+ if (id === 'capabilities')
60
+ return _jsx(Capabilities, {});
61
+ if (id === 'memory')
62
+ return _jsx(MemorySettings, {});
63
+ return (_jsx(Text, { fontSize: "$3", color: "$soft", children: note ?? 'Nothing to configure here yet.' }));
64
+ }
65
+ /**
66
+ * One default in one place; agents may still bring their own model. A premium
67
+ * model is never a default: it is picked per conversation, in the composer, so
68
+ * this picker offers the rest of the catalog.
69
+ */
70
+ function Model() {
71
+ const { models: served } = useModels();
72
+ const models = useMemo(() => parseModels(served).filter((m) => m.class !== 'premium'), [served]);
73
+ // The one choice Chat and Dev share (@hanzo/build): changed here, changed in both composers.
74
+ const [choice, choose] = useChoice();
75
+ return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats and new runs. An agent with its own model keeps that choice. Premium models are picked per conversation in the composer." }), _jsx(XStack, { self: "flex-start", children: _jsx(ModelPicker, { models: models, scope: "chat", value: choice.model, onChange: (id) => choose({ model: id }), size: "sm" }) })] }));
76
+ }
77
+ /**
78
+ * WHAT THE GATEWAY SERVES, read from the same `/v1/models` the rooms read.
79
+ *
80
+ * Counted, never typed: the catalog moves without this file, so a number written
81
+ * here is wrong by the next deploy. An empty answer says the account reaches
82
+ * nothing rather than drawing a zero, and the mark comes from the model's own
83
+ * id so a family wears the logo of the lab that makes it.
84
+ */
85
+ function Served() {
86
+ const { models, loading, error } = useModels();
87
+ if (loading) {
88
+ return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading the catalog." });
89
+ }
90
+ if (error) {
91
+ return _jsxs(Text, { fontSize: "$3", color: "$soft", children: ["The catalog did not answer: ", error.message] });
92
+ }
93
+ if (models.length === 0) {
94
+ return (_jsx(Text, { fontSize: "$3", color: "$soft", children: "No model answers on this account. Sign in to reach the ones your org opens." }));
95
+ }
96
+ return (_jsxs(YStack, { gap: "$3", children: [_jsxs(Text, { fontSize: "$3", color: "$soft", children: [models.length, " models answer at /v1/models on this account."] }), _jsx(YStack, { children: models.map((m) => (_jsxs(XStack, { items: "center", gap: "$2.5", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(ProviderMark, { model: m.id, org: m.owned_by, size: 16, ink: true }), _jsx(Text, { fontSize: "$2", color: "$ink", flex: 1, children: m.id }), m.owned_by ? _jsx(Text, { fontSize: "$1", color: "$soft", children: m.owned_by }) : null] }, m.id))) }), _jsx(XStack, { children: _jsx(Box, { render: _jsx("a", { href: site('/models') }), ...tap, children: _jsxs(XStack, { items: "center", gap: "$2", py: "$2", children: [_jsx(Text, { fontSize: "$3", color: "$ink", children: "Browse the model catalog" }), _jsx(ArrowUpRight, { size: 13, "aria-hidden": true })] }) }) })] }));
97
+ }
98
+ /** Who is signed in, and where they can act. */
99
+ function Account() {
100
+ const identity = useIamIdentity();
101
+ const { isAuthenticated } = useIam();
102
+ const logout = useSignOut();
103
+ const { organizations, currentOrgId, currentRole, switchOrg } = useOrganizations();
104
+ if (!isAuthenticated || !identity) {
105
+ return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your account." });
106
+ }
107
+ return (_jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(YStack, { width: 44, height: 44, rounded: 22, bg: "$borderColor", items: "center", justify: "center", children: _jsx(Text, { fontSize: "$3", color: "$ink", children: identity.initials }) }), _jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$4", color: "$ink", children: identity.name }), identity.email ? _jsx(Text, { fontSize: "$2", color: "$soft", children: identity.email }) : null] })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Organizations" }), organizations.length === 0 ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "You are not a member of an organization." })) : (organizations.map((o) => {
108
+ const here = o.name === currentOrgId;
109
+ return (_jsx(Box, { render: _jsx("button", { type: "button" }), onClick: () => { pick(o.name); switchOrg(o.name); }, ...tap, children: _jsxs(XStack, { items: "center", justify: "space-between", py: "$2", width: "100%", gap: "$2", children: [_jsx(Text, { fontSize: "$3", color: here ? '$ink' : '$soft', children: o.displayName || o.name }), here && currentRole ? _jsx(Text, { fontSize: "$2", color: "$soft", children: currentRole }) : null] }) }, o.name));
110
+ }))] }), _jsx(Separator, {}), _jsx(XStack, { children: _jsx(Button, { size: "sm", onClick: () => logout(), children: "Sign out" }) })] }));
111
+ }
112
+ /** Where the plan's and billing's reads go: this browser's bearer, in its organization. */
113
+ function useTarget() {
114
+ const scoped = org();
115
+ return useMemo(() => ({ api: api(), token: bearer, org: scoped }), [scoped]);
116
+ }
117
+ /**
118
+ * The plan and its term, then the credits — @hanzo/build's `Credits`, the one
119
+ * view every Billing page draws: the balance, grants, auto-reload, this month's
120
+ * spend and its limit, said to be separate from the plan's usage. Money moves on
121
+ * the payment host, so adding funds and a card both open it.
122
+ */
123
+ function Billing() {
124
+ const { isAuthenticated } = useIam();
125
+ const t = useTarget();
126
+ const { tier } = useTier(isAuthenticated, org());
127
+ const sub = useSubscription(isAuthenticated, org());
128
+ if (!isAuthenticated)
129
+ return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see billing." });
130
+ // WHAT THE CUSTOMER BOUGHT, by its family: `Max` with `20x` beside it.
131
+ const plan = tier ? label(tier.plan, tier.tier.name) ?? { name: 'Free', tag: '' } : null;
132
+ return (_jsxs(YStack, { gap: "$5", children: [plan ? (_jsxs(YStack, { gap: "$1", "data-slot": "billing-plan", children: [_jsx(Title, { label: plan, size: "$5" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: sub ? renewal(sub) : tier?.plan ? 'Your plan.' : 'No paid plan. Usage is paid from credits.' })] })) : (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading your plan\u2026" })), _jsx(Credits, { target: t, admin: administers(bearer(), org()), add: payPage() }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Payment" }), _jsx(Text, { fontSize: "$3", color: "$soft", children: "A card is entered on the payment host and nowhere else, so this is the one control here that opens another surface." }), _jsx(XStack, { children: _jsx(Box, { render: _jsx("a", { href: payPage(), target: "_blank", rel: "noreferrer" }), ...tap, children: _jsxs(XStack, { items: "center", gap: "$2", py: "$2", children: [_jsx(Text, { fontSize: "$3", color: "$ink", children: "Manage plan, payment method and invoices" }), _jsx(ExternalLink, { size: 13, "aria-hidden": true })] }) }) })] })] }));
133
+ }
134
+ /**
135
+ * Usage: @hanzo/build's `Plan`, the one view every Usage page draws — the plan's
136
+ * windows and each class of model's share, as shares; the free allowance on
137
+ * Free; the balance with no plan — and the choice of paying from credits once
138
+ * the allowance runs out. A reader with no paid plan also sees what was spent,
139
+ * over time (@hanzo/usage), because money is their meter.
140
+ */
141
+ function Usage() {
142
+ const { accessToken, isAuthenticated } = useIam();
143
+ const t = useTarget();
144
+ const read = useLimits(isAuthenticated, org());
145
+ if (!isAuthenticated || !accessToken) {
146
+ return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
147
+ }
148
+ const plan = _jsx(Plan, { target: t, onPlans: () => window.location.assign(site('/pricing')), onBilling: () => showSettings('billing') });
149
+ if (!spendShown(read))
150
+ return plan;
151
+ // The address is read HERE rather than at import: the panel sends the bearer
152
+ // with every read, and a credentialed read to an absolute address from a page
153
+ // the gateway does not admit dies in preflight — which drew this panel as an
154
+ // account that had spent nothing.
155
+ return (_jsxs(YStack, { gap: "$6", children: [plan, _jsx(UsagePanel, { baseUrl: api(), token: accessToken, upgrade: payPage(), title: "Spend" })] }));
156
+ }
157
+ const HISTORICAL_PERSONAS = [
158
+ { id: 'architect', name: 'Software Architect', role: 'Full-stack & systems design' },
159
+ { id: 'satoshi', name: 'Satoshi Nakamoto', role: 'Distributed cryptography & consensus' },
160
+ { id: 'turing', name: 'Alan Turing', role: 'Algorithmic logic & formal verification' },
161
+ { id: 'ada', name: 'Ada Lovelace', role: 'Computational precision & analysis' },
162
+ { id: 'davinci', name: 'Leonardo da Vinci', role: 'Polymathic engineering & design' },
163
+ ];
164
+ function AgentSettings() {
165
+ const [instructions, setInstructions] = useState(() => {
166
+ if (typeof window === 'undefined')
167
+ return '';
168
+ return localStorage.getItem('hanzo.agent.instructions') || 'You are an autonomous AI software engineer at Hanzo. Think step-by-step and write robust code.';
169
+ });
170
+ const [persona, setPersona] = useState(() => {
171
+ if (typeof window === 'undefined')
172
+ return 'architect';
173
+ return localStorage.getItem('hanzo.agent.persona') || 'architect';
174
+ });
175
+ const [autonomy, setAutonomy] = useState(() => {
176
+ if (typeof window === 'undefined')
177
+ return 'autonomous';
178
+ return localStorage.getItem('hanzo.agent.autonomy') || 'autonomous';
179
+ });
180
+ const [saved, setSaved] = useState(false);
181
+ const save = () => {
182
+ localStorage.setItem('hanzo.agent.instructions', instructions);
183
+ localStorage.setItem('hanzo.agent.persona', persona);
184
+ localStorage.setItem('hanzo.agent.autonomy', autonomy);
185
+ setSaved(true);
186
+ setTimeout(() => setSaved(false), 2000);
187
+ };
188
+ return (_jsxs(YStack, { gap: "$4", maxW: 480, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Nothing is bound to this surface yet \u2014 when you bind a personal agent, its persona, tools and memory live here rather than in a shared team." }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "Persona & Historical Character" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Pick an intellectual archetype for agent reasoning and tone." }), _jsx(XStack, { flexWrap: "wrap", gap: "$2", pt: "$1", children: HISTORICAL_PERSONAS.map((p) => {
189
+ const active = persona === p.id;
190
+ return (_jsx(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => setPersona(p.id), ...CHOICE, bg: active ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: active ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', py: 6, px: 12, children: _jsx(Text, { fontSize: "$2", fontWeight: active ? '600' : '400', color: active ? '$ink' : '$soft', children: p.name }) }, p.id));
191
+ }) })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "System Prompt Instructions" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Custom system directions prepended to all coding and research runs." }), _jsx(Text, { render: _jsx("textarea", { value: instructions, onChange: (e) => setInstructions(e.target.value), rows: 4 }), width: "100%", bg: "var(--panel, color-mix(in srgb, var(--pure-black) 30%, transparent))", color: "inherit", borderWidth: 1, borderColor: "var(--borderColor, var(--white-15))", rounded: 8, p: 10, fontSize: "$2", fontFamily: "$mono" })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "Autonomy & Permissions" }), _jsxs(XStack, { gap: "$2", children: [_jsxs(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => setAutonomy('autonomous'), ...CHOICE, flex: 1, p: 10, bg: autonomy === 'autonomous' ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: autonomy === 'autonomous' ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Autonomous" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Executes tools & writes code without interrupting." })] }), _jsxs(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => setAutonomy('supervised'), ...CHOICE, flex: 1, p: 10, bg: autonomy === 'supervised' ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: autonomy === 'supervised' ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Supervised" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Requests confirmation before file writes and commands." })] })] })] }), _jsx(XStack, { self: "flex-start", pt: "$2", children: _jsx(Button, { size: "sm", onClick: save, children: saved ? 'Saved' : 'Save agent preferences' }) })] }));
192
+ }
193
+ /**
194
+ * WHAT A RUN MAY REACH FOR — the four capabilities, described and not measured.
195
+ *
196
+ * Each row used to carry a badge reading "Enabled", and the MCP row read
197
+ * "Active (:8080/mcp)". Nothing asked. No call stands behind any of the four,
198
+ * so the badges said "on" for a reader whose run had none of it, and the port
199
+ * was one developer's machine printed on everybody's settings pane. A green
200
+ * light nobody wired is worse than no light: it is read as a check that passed.
201
+ *
202
+ * These are facts about the PRODUCT — what a run is allowed to do — which is
203
+ * what this pane is for. Whether a given run got them is a fact about that run,
204
+ * and the run is where it belongs.
205
+ */
206
+ function Capabilities() {
207
+ return (_jsxs(YStack, { gap: "$3", maxW: 480, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "What models and agent swarms are granted access to during tasks." }), _jsx(YStack, { gap: "$2", pt: "$1", children: [
208
+ { title: 'Filesystem (Read / Write)', desc: 'Create, modify, and delete workspace files in the target repository.' },
209
+ { title: 'Shell & Terminal Exec', desc: 'Execute build, test, and shell commands in isolated sandbox VMs.' },
210
+ { title: 'Live Web & Search', desc: 'Query internet search APIs and fetch web documentation.' },
211
+ { title: 'MCP Protocol Server', desc: 'Expose local and remote tools via Model Context Protocol at /mcp.' },
212
+ ].map((cap) => (_jsxs(YStack, { gap: "$0.5", p: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: cap.title }), _jsx(Text, { fontSize: "$2", color: "$soft", children: cap.desc })] }, cap.title))) })] }));
213
+ }
214
+ function MemorySettings() {
215
+ const [cleared, setCleared] = useState(false);
216
+ const clearMemory = () => {
217
+ localStorage.removeItem('hanzo.chat.history');
218
+ setCleared(true);
219
+ setTimeout(() => setCleared(false), 2000);
220
+ };
221
+ return (_jsxs(YStack, { gap: "$3", maxW: 480, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Semantic memory and conversation recall across sessions." }), _jsxs(YStack, { gap: "$0.5", p: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: "Semantic Vector Index" }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Automatically indexes past workspace sessions into vector memory." })] }), _jsx(XStack, { self: "flex-start", pt: "$2", children: _jsx(Button, { size: "sm", variant: "outline", onClick: clearMemory, children: cleared ? 'Memory cleared' : 'Clear local memory cache' }) })] }));
222
+ }
package/dist/Shell.js CHANGED
@@ -13,7 +13,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
13
13
  // holds arrangement and words and owns no pixels of its own. That is the whole
14
14
  // architecture: a stranger with `@hanzo/ui`, `@hanzo/ai` and `@hanzo/iam` can
15
15
  // assemble this room, because nothing in it is private to hanzo.ai.
16
- import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
16
+ import { createContext, lazy, Suspense, useContext, useEffect, useMemo, useRef, useState, } from "react";
17
17
  import { createPortal } from "react-dom";
18
18
  import { MessageSquare, Code2, Inbox, Bot, ListChecks, HardDrive, Settings2, UserPlus, Building2, Search, Hash, AtSign, Lock, Workflow, Plus, ArrowLeft, ArrowRight, Send, Minus, Blocks, Star, FolderOpen, Activity, BookOpen, SquareTerminal, Share2, Sparkles, PanelLeft, PanelRight, Users, Globe, Cpu, Boxes, Wallet, Video, Calendar, LayoutGrid, ArrowUpLeft, } from "lucide-react";
19
19
  import { AiProvider, useAgents, useInbox, useModels, usePeople, } from "@hanzo/ai/react";
@@ -70,7 +70,7 @@ import { roomName } from "./network.js";
70
70
  import { useTeamRooms, teamKey, roomHref } from "./rooms.js";
71
71
  import { apex, app, dev, ENTRY, room as moves, WORKSPACE } from "./lib/host.js";
72
72
  import { netKey } from "./conversations.js";
73
- import { FIRST, SettingsList, SettingsPane, } from "./Settings.js";
73
+ import { FIRST, SettingsList, } from "./Settings.js";
74
74
  import { Profile } from "./Profile.js";
75
75
  import { SupportPicker } from "./Support.js";
76
76
  import { Find } from "./Find.js";
@@ -116,6 +116,8 @@ import { getStoredPeople, getStoredAgents, getCustomThreads, createDirectChat, }
116
116
  import { bare } from './lib/bare.js';
117
117
  import { BAD } from './lib/mix.js';
118
118
  import { brand, site, where, wordmark } from './where.js';
119
+ /** The room while a settings row is open, loaded when one is: none of it is a room's first screen. */
120
+ const SettingsPane = lazy(() => import('./SettingsPane.js').then((m) => ({ default: m.SettingsPane })));
119
121
  /**
120
122
  * The destinations, in rail order.
121
123
  *
@@ -1235,5 +1237,5 @@ function Frame({ mode, children, navigate, back, forward, }) {
1235
1237
  }, position: "relative", width: "100%", height: "100dvh", overflow: "hidden", bg: "$background", backgroundImage: "var(--pane-ground)", children: [_jsx(Rail, { mode: mode, places: places, open: asked, team: onApex ? (signedIn ? ENTRY : WORKSPACE) : undefined, navigate: navigate, onSettings: () => showSettings(FIRST), onMore: () => setFinding(true) }), _jsxs(XStack, { render: "header", "data-slot": "bar", gridArea: "bar", ...sheet(0), height: 44, shrink: 0, items: "center", px: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "var(--edge, var(--white-06))", children: [_jsxs(XStack, { items: "center", gap: "$1", shrink: 0, children: [_jsx(SidebarIconButton, { ...TAP, label: shown ? "Close sidebar" : "Open sidebar", onPress: toggle, onMouseEnter: () => setPeeking(true), children: _jsx(PanelLeft, { size: 16, "aria-hidden": true }) }), back || forward ? (_jsxs(XStack, { display: "none", $md: { display: "flex" }, items: "center", gap: "$1", shrink: 0, children: [_jsx(SidebarIconButton, { ...TAP, label: "Back", onPress: () => back?.(), children: _jsx(ArrowLeft, { size: 16, "aria-hidden": true }) }), _jsx(SidebarIconButton, { ...TAP, label: "Forward", onPress: () => forward?.(), children: _jsx(ArrowRight, { size: 16, "aria-hidden": true }) })] })) : null] }), _jsx(XStack, { flex: 1, justify: "center", children: _jsx(YStack, { width: 440, maxW: "100%", position: "relative", children: _jsxs(Box, { render: "button", onClick: () => setFinding(true), display: "flex", flexDirection: "row", "data-slot": "find", items: "center", "$max-md": { justify: "center" }, gap: "$2", width: "100%", height: CONTROL, px: "$2.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsxs(Text, { fontSize: "$1", color: "$soft", "$max-md": { display: "none" }, children: ["Search ", workspace] }), _jsx(XStack, { ml: "auto", items: "center", justify: "center", height: 18, px: "$1", rounded: "$1", bg: "$edge", "$max-md": { display: "none" }, children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$soft", children: "\u2318K" }) })] }) }) }), _jsxs(XStack, { shrink: 0, minW: 0, justify: "flex-end", items: "center", gap: "$2", children: [settled && !signedIn ? (_jsx(Box, { ...TAP, render: "button", onClick: () => enter(LOGIN), display: "flex", items: "center", justify: "center", cursor: "pointer", children: _jsx(XStack, { items: "center", justify: "center", height: CONTROL, px: "$2.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$ink", children: "Sign in" }) }) })) : null, _jsxs(XStack, { display: "none", $lg: { display: "flex" }, items: "center", gap: "$1", children: [_jsx(SidebarIconButton, { ...TAP, label: aside === "roster" ? "Hide directory & roster" : "Show directory & roster", onPress: () => showAside(aside === "roster" ? null : "roster"), children: _jsx(Users, { size: 16, "aria-hidden": true }) }), _jsx(SidebarIconButton, { ...TAP, label: aside === "tasks" ? "Close side panel" : "Open side panel", onPress: () => showAside(aside === "tasks" ? null : "tasks"), children: _jsx(PanelRight, { size: 16, "aria-hidden": true }) })] })] })] }), shown ? (_jsx(Box, { render: "button", "aria-label": "Close sidebar", onClick: dismiss, ...scrim, "data-slot": "scrim", position: "absolute", t: 44, r: 0, b: TABS_HEIGHT, l: 0, z: "var(--z-scrim)", $md: { display: "none" } })) : null, shown ? (_jsx(XStack, { "data-slot": "list", gridArea: "list", position: asked ? "relative" : "absolute", ...(asked ? {} : { top: 44, bottom: 0, left: 64 }), "$max-md": { position: "fixed", t: 44, r: 0, b: TABS_HEIGHT, l: 0, width: "100%" }, z: "var(--z-drawer)", bg: "$background", onMouseEnter: () => setPeeking(true), onMouseLeave: () => {
1236
1238
  if (!asked)
1237
1239
  setPeeking(false);
1238
- }, children: list })) : null, _jsx(Paned, { swap: settingsPane ? _jsx(SettingsPane, { id: settingsPane }) : undefined, children: children }), _jsx(Profile, { open: profiling, onClose: () => setProfiling(false) }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null, finding ? (_jsx(Find, { doors: doors, onClose: () => setFinding(false), navigate: navigate })) : null] }));
1240
+ }, children: list })) : null, _jsx(Paned, { swap: settingsPane ? _jsx(Suspense, { fallback: null, children: _jsx(SettingsPane, { id: settingsPane }) }) : undefined, children: children }), _jsx(Profile, { open: profiling, onClose: () => setProfiling(false) }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null, finding ? (_jsx(Find, { doors: doors, onClose: () => setFinding(false), navigate: navigate })) : null] }));
1239
1241
  }
@@ -0,0 +1,4 @@
1
+ export declare function Standing({ onPlans, onBilling }: {
2
+ onPlans: () => void;
3
+ onBilling: () => void;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ // The plan at the head of the account menu (Account.tsx): @hanzo/build's
4
+ // `Meter` over the organization's standing, read when the menu opens. Its own
5
+ // module so the menu's foot row costs a room's first screen nothing: it loads
6
+ // with the menu.
7
+ import { useMemo } from 'react';
8
+ import { Meter, useStanding } from '@hanzo/build';
9
+ import { api } from './lib/api.js';
10
+ import { bearer, org } from './lib/session.js';
11
+ export function Standing({ onPlans, onBilling }) {
12
+ const scoped = org();
13
+ const target = useMemo(() => ({ api: api(), token: bearer, org: scoped }), [scoped]);
14
+ const plan = useStanding(target, true);
15
+ return _jsx(Meter, { read: plan, onPlans: onPlans, onBilling: onBilling });
16
+ }
@@ -51,6 +51,8 @@ export declare const DEV = "/dev";
51
51
  * hanzo.ai/studio, whose page draws the app for a signed-in reader.
52
52
  */
53
53
  export declare const STUDIO = "/studio";
54
+ /** Studio at an address of its own (@hanzo/studio `route`): '' is New, /studio. */
55
+ export declare const studio: (at: string) => string;
54
56
  /** Chat on a conversation, or /chat itself for a new one. */
55
57
  export declare const talk: (id?: string | null) => string;
56
58
  /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
@@ -62,6 +64,9 @@ export type Place = {
62
64
  } | {
63
65
  mode: 'dev';
64
66
  at: string;
67
+ } | {
68
+ mode: 'studio';
69
+ at: string;
65
70
  };
66
71
  /** The place a path names, or null when it names none of the app's. */
67
72
  export declare function place(pathname: string): Place | null;
package/dist/lib/host.js CHANGED
@@ -54,6 +54,8 @@ export const DEV = '/dev';
54
54
  * hanzo.ai/studio, whose page draws the app for a signed-in reader.
55
55
  */
56
56
  export const STUDIO = '/studio';
57
+ /** Studio at an address of its own (@hanzo/studio `route`): '' is New, /studio. */
58
+ export const studio = (at) => (at ? `${STUDIO}/${at.replace(/^\/+/, '')}` : STUDIO);
57
59
  /** Chat on a conversation, or /chat itself for a new one. */
58
60
  export const talk = (id) => (id ? `${CHAT}/${encodeURIComponent(id)}` : CHAT);
59
61
  /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
@@ -67,6 +69,8 @@ export function place(pathname) {
67
69
  return { mode: 'chat', thread: null };
68
70
  if (p === DEV || p.startsWith(`${DEV}/`))
69
71
  return { mode: 'dev', at: path(parse(p.slice(DEV.length))) };
72
+ if (p === STUDIO || p.startsWith(`${STUDIO}/`))
73
+ return { mode: 'studio', at: p.slice(STUDIO.length + 1) };
70
74
  if (!p.startsWith(`${CHAT}/`))
71
75
  return null;
72
76
  const id = p.slice(CHAT.length + 1);
@@ -136,7 +140,8 @@ export function remember(mode) {
136
140
  /** Where `/` takes a member: the mode they were last in, Chat when none is kept. */
137
141
  export function resume() {
138
142
  try {
139
- return window.localStorage.getItem(MODE) === 'dev' ? DEV : CHAT;
143
+ const kept = window.localStorage.getItem(MODE);
144
+ return kept === 'dev' ? DEV : kept === 'studio' ? STUDIO : CHAT;
140
145
  }
141
146
  catch {
142
147
  return CHAT;
package/dist/where.d.ts CHANGED
@@ -68,6 +68,6 @@ export declare const where: () => Addresses;
68
68
  /** The product's name, as the host states it. Read at call time, like every address. */
69
69
  export declare const brand: () => string;
70
70
  /** The product's name in each mode of the app: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
71
- export declare const wordmark: (mode: 'chat' | 'dev') => string;
71
+ export declare const wordmark: (mode: 'chat' | 'dev' | 'studio') => string;
72
72
  /** A page of the app's own, from wherever the rooms are mounted. */
73
73
  export declare const site: (path: string) => string;
package/dist/where.js CHANGED
@@ -12,6 +12,6 @@ export const where = () => addresses;
12
12
  /** The product's name, as the host states it. Read at call time, like every address. */
13
13
  export const brand = () => addresses.brand ?? 'Hanzo AI';
14
14
  /** The product's name in each mode of the app: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
15
- export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : brand());
15
+ export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : mode === 'studio' ? 'Hanzo Studio' : brand());
16
16
  /** A page of the app's own, from wherever the rooms are mounted. */
17
17
  export const site = (path) => `${addresses.site}${path}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
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",
@@ -55,6 +55,7 @@
55
55
  "@hanzo/gui": "^8.3.5",
56
56
  "@hanzo/iam": ">=0.21.28",
57
57
  "@hanzo/logo": ">=1.0.26",
58
+ "@hanzo/studio": ">=0.1.0",
58
59
  "@hanzo/ui": ">=8.27.67",
59
60
  "@hanzo/usage": ">=0.1.18",
60
61
  "@hanzo/voice": ">=0.1.16",
@@ -73,6 +74,7 @@
73
74
  "@hanzo/gui": "8.3.5",
74
75
  "@hanzo/iam": "0.21.28",
75
76
  "@hanzo/logo": "1.0.26",
77
+ "@hanzo/studio": "0.1.0",
76
78
  "@hanzo/ui": "8.27.67",
77
79
  "@hanzo/usage": "0.1.18",
78
80
  "@hanzo/voice": "0.1.16",