@hanzo/rooms 0.1.67 → 0.1.68

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
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.67",
3
+ "version": "0.1.68",
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",