@hanzo/rooms 0.1.38 → 0.1.39

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/Settings.js CHANGED
@@ -21,7 +21,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
21
21
  //
22
22
  // The panes that are not yet built say so in one sentence rather than pretending
23
23
  // to a screen. An honest empty pane is a smaller lie than an invented one.
24
- import { dev } from './lib/host.js';
24
+ import { app, dev } from './lib/host.js';
25
25
  import { useMemo, useState } from 'react';
26
26
  import { ArrowLeft, ArrowUpRight, ExternalLink } from 'lucide-react';
27
27
  import { Box, Button, Separator, Text, XStack, YStack, } from '@hanzo/ui';
@@ -33,17 +33,16 @@ import { useModels } from '@hanzo/ai/react';
33
33
  import { ModelPicker } from '@hanzo/ui/models';
34
34
  import { parseModels } from '@hanzo/ui/models/catalog';
35
35
  import { UsagePanel } from '@hanzo/usage/panel';
36
+ import { administers, Credits, label, Plan, Title, useChoice } from '@hanzo/build';
36
37
  import { Catalog } from './Directory.js';
37
- import { useModel } from './model.js';
38
- import { checkoutUrl, payPage } from './lib/pay.js';
39
- import { planName } from './lib/plans.js';
38
+ import { payPage } from './lib/pay.js';
40
39
  import { api } from './lib/api.js';
41
40
  import { renewal, useSubscription, useTier } from './lib/tier.js';
42
- import { paidPlan, setCreditsAfterAllowance, spendShown, useLimits } from './lib/limits.js';
43
- import { PlanUsage } from '@hanzo/ui/product/PlanUsage';
41
+ import { spendShown, useLimits } from './lib/limits.js';
42
+ import { showSettings } from './open.js';
44
43
  import { Look } from './look.js';
45
44
  import { ProviderMark } from './ProviderMark.js';
46
- import { org, pick } from './lib/session.js';
45
+ import { bearer, org, pick } from './lib/session.js';
47
46
  import { site } from './where.js';
48
47
  /** A choice among a few: a hairline edge and a corner. The fill and the edge's
49
48
  * ink are the call site's, because they say which one is chosen. */
@@ -77,13 +76,15 @@ const GROUPS = [
77
76
  },
78
77
  ],
79
78
  },
79
+ // Plugins and skills are what an agent brings to a run, and Customize is
80
+ // their one home: these rows lead there rather than drawing a second list.
80
81
  {
81
82
  label: 'Customize',
82
83
  rows: [
83
84
  { id: 'apps', label: 'Apps' },
84
85
  { id: 'channels', label: 'Channels' },
85
- { id: 'plugins', label: 'Plugins' },
86
- { id: 'skills', label: 'Skills' },
86
+ { id: 'plugins', label: 'Plugins', href: site(app('-/customize/plugins')) },
87
+ { id: 'skills', label: 'Skills', href: site(app('-/customize')) },
87
88
  { id: 'memory', label: 'Memory', note: 'Nothing is remembered between conversations yet.' },
88
89
  ],
89
90
  },
@@ -145,9 +146,8 @@ export function SettingsPane({ id }) {
145
146
  }
146
147
  /** One pane. */
147
148
  function Pane({ id, note }) {
148
- if (id === 'apps' || id === 'channels' || id === 'plugins' || id === 'skills') {
149
+ if (id === 'apps' || id === 'channels')
149
150
  return _jsx(Catalog, { tab: id });
150
- }
151
151
  if (id === 'general')
152
152
  return _jsx(Look, {});
153
153
  if (id === 'account')
@@ -155,7 +155,7 @@ function Pane({ id, note }) {
155
155
  if (id === 'billing')
156
156
  return _jsx(Billing, {});
157
157
  if (id === 'usage')
158
- return _jsx(Usage, { meters: true });
158
+ return _jsx(Usage, {});
159
159
  if (id === 'keys')
160
160
  return _jsx(Keys, {});
161
161
  if (id === 'model')
@@ -178,8 +178,9 @@ function Pane({ id, note }) {
178
178
  function Model() {
179
179
  const { models: served } = useModels();
180
180
  const models = useMemo(() => parseModels(served).filter((m) => m.class !== 'premium'), [served]);
181
- const [model, choose] = useModel();
182
- return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats. 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: model, onChange: choose, size: "sm" }) })] }));
181
+ // The one choice Chat and Dev share (@hanzo/build): changed here, changed in both composers.
182
+ const [choice, choose] = useChoice();
183
+ 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" }) })] }));
183
184
  }
184
185
  /**
185
186
  * WHAT THE GATEWAY SERVES, read from the same `/v1/models` the rooms read.
@@ -216,37 +217,50 @@ function Account() {
216
217
  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));
217
218
  }))] }), _jsx(Separator, {}), _jsx(XStack, { children: _jsx(Button, { size: "sm", onClick: () => logout(), children: "Sign out" }) })] }));
218
219
  }
219
- /** The plan, and the one control that has to leave. */
220
+ /** Where the plan's and billing's reads go: this browser's bearer, in its organization. */
221
+ function useTarget() {
222
+ const scoped = org();
223
+ return useMemo(() => ({ api: api(), token: bearer, org: scoped }), [scoped]);
224
+ }
225
+ /**
226
+ * The plan and its term, then the credits — @hanzo/build's `Credits`, the one
227
+ * view every Billing page draws: the balance, grants, auto-reload, this month's
228
+ * spend and its limit, said to be separate from the plan's usage. Money moves on
229
+ * the payment host, so adding funds and a card both open it.
230
+ */
220
231
  function Billing() {
221
232
  const { isAuthenticated } = useIam();
233
+ const t = useTarget();
222
234
  const { tier } = useTier(isAuthenticated, org());
223
235
  const sub = useSubscription(isAuthenticated, org());
224
- // WHAT THE CUSTOMER BOUGHT, and its term: a plan is a subscription with usage
225
- // limits, so its line is the price and the renewal, never a balance.
226
- const plan = tier ? planName(tier.plan) || tier.tier.displayName || tier.tier.name : null;
227
- return (_jsxs(YStack, { gap: "$4", children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "What this account is billed for, and what it has spent." }), plan ? (_jsx(Text, { fontSize: "$4", color: "$ink", children: sub ? `${plan} · ${renewal(sub)}` : plan })) : null, _jsx(Usage, { sections: { overview: true, chart: false, categories: true, breakdown: true, activity: false }, title: "Spend" }), _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 and payment method" }), _jsx(ExternalLink, { size: 13, "aria-hidden": true })] }) }) })] })] }));
236
+ if (!isAuthenticated)
237
+ return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see billing." });
238
+ // WHAT THE CUSTOMER BOUGHT, by its family: `Max` with `20x` beside it.
239
+ const plan = tier ? label(tier.plan, tier.tier.name) ?? { name: 'Free', tag: '' } : null;
240
+ 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 })] }) }) })] })] }));
228
241
  }
229
242
  /**
230
- * The one usage panel every Hanzo product renders, reading as this account — for a
231
- * reader with no plan. A plan holder's usage is the plan's shares (`meters` draws
232
- * them, with the plan's terms and its actions, where usage is the subject), never
233
- * money (spendShown).
243
+ * Usage: @hanzo/build's `Plan`, the one view every Usage page draws — the plan's
244
+ * windows and each class of model's share, as shares; the free allowance on
245
+ * Free; the balance with no plan — and the choice of paying from credits once
246
+ * the allowance runs out. A reader with no paid plan also sees what was spent,
247
+ * over time (@hanzo/usage), because money is their meter.
234
248
  */
235
- function Usage({ sections, title, meters = false }) {
249
+ function Usage() {
236
250
  const { accessToken, isAuthenticated } = useIam();
251
+ const t = useTarget();
237
252
  const read = useLimits(isAuthenticated, org());
238
253
  if (!isAuthenticated || !accessToken) {
239
254
  return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
240
255
  }
241
- const plan = meters && read.limits ? (_jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice, onCredits: (on) => setCreditsAfterAllowance(on).then(read.reload), creditsHref: payPage(), addCreditsHref: checkoutUrl() })) : null;
242
- if (paidPlan(read.limits) || !spendShown(read))
256
+ const plan = _jsx(Plan, { target: t, onPlans: () => window.location.assign(site('/pricing')), onBilling: () => showSettings('billing') });
257
+ if (!spendShown(read))
243
258
  return plan;
244
259
  // The address is read HERE rather than at import: the panel sends the bearer
245
260
  // with every read, and a credentialed read to an absolute address from a page
246
261
  // the gateway does not admit dies in preflight — which drew this panel as an
247
262
  // account that had spent nothing.
248
- const spend = _jsx(UsagePanel, { baseUrl: api(), token: accessToken, sections: sections, title: title, upgrade: payPage() });
249
- return plan ? (_jsxs(YStack, { gap: "$6", children: [plan, spend] })) : (spend);
263
+ return (_jsxs(YStack, { gap: "$6", children: [plan, _jsx(UsagePanel, { baseUrl: api(), token: accessToken, upgrade: payPage(), title: "Spend" })] }));
250
264
  }
251
265
  /** Keys are minted and revoked through the console, which holds the one issuer. */
252
266
  function Keys() {
package/dist/Shell.js CHANGED
@@ -68,7 +68,7 @@ import { Face } from "./cast.js";
68
68
  import { useStarred, titleOf } from "./stars.js";
69
69
  import { roomName } from "./network.js";
70
70
  import { useTeamRooms, teamKey, roomHref } from "./rooms.js";
71
- import { apex, dev, ENTRY, room as moves, WORKSPACE } from "./lib/host.js";
71
+ import { apex, app, dev, ENTRY, room as moves, WORKSPACE } from "./lib/host.js";
72
72
  import { netKey } from "./conversations.js";
73
73
  import { FIRST, SettingsList, SettingsPane, } from "./Settings.js";
74
74
  import { Profile } from "./Profile.js";
@@ -1083,33 +1083,30 @@ function Frame({ mode, children, navigate, back, forward, }) {
1083
1083
  /**
1084
1084
  * THE SURFACES THAT ARE NOT ROOMS, said once.
1085
1085
  *
1086
- * Connectors is a page, Skills and Usage are panes of Settings, Terminals is
1087
- * another product at its own host, Docs is a page. The column groups them
1088
- * under "More" — where a reader coming from hanzo.app looks — and the palette
1089
- * offers the same set, so this says WHAT they are and each surface says only
1090
- * how it draws them. Written twice they had already parted: Settings was in
1091
- * one list and not the other.
1086
+ * Customize is Dev's page, Usage a pane of Settings, Terminals another
1087
+ * product at its own host, Docs a page. The column draws each as a
1088
+ * row of its own under the quiet label Setup, right under the rooms — nothing
1089
+ * waits behind a More — and the palette offers the same set, so this says
1090
+ * WHAT they are and each surface says only how it draws them. Written twice
1091
+ * they had already parted: Settings was in one list and not the other.
1092
1092
  *
1093
1093
  * ONE VERB PER ROW, and no href beside it. A row that carried both would be
1094
1094
  * two ways to reach one place, and the two would answer differently the day
1095
1095
  * one of them learned to close the sheet behind it.
1096
1096
  */
1097
1097
  const tools = useMemo(() => [
1098
+ // CUSTOMIZE is the one home of what an agent brings to a run — skills,
1099
+ // connectors, plugins, agents — in Dev (@hanzo/build). One row leads
1100
+ // there; a second list here would be the second copy that drifts.
1098
1101
  {
1099
- id: "connectors",
1100
- label: "Connectors",
1102
+ id: "customize",
1103
+ label: "Customize",
1101
1104
  icon: Blocks,
1102
1105
  go: () => {
1103
- navigate?.(site("/integrations"));
1106
+ navigate?.(site(app("-/customize")));
1104
1107
  dismiss();
1105
1108
  },
1106
1109
  },
1107
- {
1108
- id: "skills",
1109
- label: "Skills",
1110
- icon: Workflow,
1111
- go: () => showSettings("skills"),
1112
- },
1113
1110
  // TERMINALS ARE TABS, and Tabs is its own product at its own host —
1114
1111
  // hanzo.app's row pointed there too. A new tab for the reason that row
1115
1112
  // used one: a reader who follows it inside this one loses the project
@@ -1200,7 +1197,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
1200
1197
  // to disagree about which one you are standing in.
1201
1198
  const workspace = currentOrg?.displayName || currentOrg?.name || "Hanzo";
1202
1199
  const owner = currentOrgId ?? "";
1203
- // The two folders' rows, narrowed by the column's filter like every other list.
1200
+ // The folder's rows and Setup's, narrowed by the column's filter like every other list.
1204
1201
  const products = CONSOLE_PRODUCTS.filter((prod) => matches(prod.label, filter));
1205
1202
  const more = tools.filter((tool) => matches(tool.label, filter));
1206
1203
  // The static export prerenders the signed-out room, so the rail's identity
@@ -1218,17 +1215,17 @@ function Frame({ mode, children, navigate, back, forward, }) {
1218
1215
  return;
1219
1216
  e.preventDefault();
1220
1217
  navigate("/");
1221
- }, "aria-label": `${brand()} home`, title: "Home" }), "data-slot": "home", display: "flex", items: "center", justify: "center", width: 28, height: 28, $touchable: { width: 40, height: 40 }, color: "inherit", children: _jsx(HanzoMark, { size: 18 }) }), settled && isAuthenticated ? (_jsx(XStack, { shrink: 1, minW: 0, children: _jsx(OrgProjectSwitcher, { ...orgState, alwaysShow: true }) })) : null] }), _jsxs(YStack, { "data-slot": "places-list", px: "$2", pb: "$2", "$max-md": { display: "none" }, children: [places.filter((place) => matches(place.label, filter)).map((place) => (_jsx(SidebarItem, { icon: _jsx(place.icon, { size: 16, "aria-hidden": true }), active: place.id === mode, onPress: () => {
1222
- navigate?.(place.route);
1223
- onPick?.();
1224
- }, children: place.label }, place.id))), matches("Settings", filter) ? (_jsx(SidebarItem, { icon: _jsx(Settings2, { size: 16, "aria-hidden": true }), onPress: () => showSettings(FIRST), children: "Settings" })) : null] }), _jsx(XStack, { px: "$3", pb: "$2", children: _jsxs(XStack, { flex: 1, height: 30, items: "center", gap: "$2", px: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 13, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: filter, onChange: (e) => setFilter(e.target.value), onKeyDown: (e) => {
1218
+ }, "aria-label": `${brand()} home`, title: "Home" }), "data-slot": "home", display: "flex", items: "center", justify: "center", width: 28, height: 28, $touchable: { width: 40, height: 40 }, color: "inherit", children: _jsx(HanzoMark, { size: 18 }) }), settled && isAuthenticated ? (_jsx(XStack, { shrink: 1, minW: 0, children: _jsx(OrgProjectSwitcher, { ...orgState, alwaysShow: true }) })) : null] }), _jsx(YStack, { "data-slot": "places-list", px: "$2", pb: "$2", "$max-md": { display: "none" }, children: places.filter((place) => matches(place.label, filter)).map((place) => (_jsx(SidebarItem, { icon: _jsx(place.icon, { size: 16, "aria-hidden": true }), active: place.id === mode, onPress: () => {
1219
+ navigate?.(place.route);
1220
+ onPick?.();
1221
+ }, children: place.label }, place.id))) }), more.length ? (_jsx(YStack, { "data-slot": "setup-list", px: "$2", pb: "$2", children: _jsx(SidebarSection, { label: "Setup", children: more.map((tool) => (_jsx(SidebarItem, { icon: _jsx(tool.icon, { size: 16, "aria-hidden": true }), onPress: tool.go, children: tool.label }, tool.id))) }) })) : null, _jsx(XStack, { px: "$3", pb: "$2", children: _jsxs(XStack, { flex: 1, height: 30, items: "center", gap: "$2", px: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 13, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: filter, onChange: (e) => setFilter(e.target.value), onKeyDown: (e) => {
1225
1222
  if (e.key === "Escape")
1226
1223
  setFilter("");
1227
1224
  },
1228
1225
  // FILTER, not Search. Two controls that both said Search did two
1229
1226
  // different things: this one narrows the list beside you, and the
1230
1227
  // bar above opens the palette that finds anything anywhere.
1231
- placeholder: "Filter", "aria-label": "Filter sidebar" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }) }), settingsPane ? (_jsx(SidebarScroll, { children: _jsx(SettingsList, { active: settingsPane, filter: filter, onPick: pick, onBack: () => showSettings(null) }) })) : (_jsx(_Fragment, { children: _jsx(SidebarScroll, { children: _jsxs(_Fragment, { children: [settled ? (_jsx(Index, { mode: mode, owner: owner, filter: filter, onPick: onPick, navigate: navigate })) : null, products.length ? (_jsx(SidebarFolder, { name: "Cloud & Platform", open: filter ? true : undefined, children: products.map((prod) => (_jsx(SidebarItem, { icon: _jsx(prod.icon, { size: 13, "aria-hidden": true }), onPress: () => window.open(prod.href, "_blank", "noopener"), children: prod.label }, prod.id))) })) : null, more.length ? (_jsx(SidebarFolder, { name: "More", open: filter ? true : undefined, children: more.map((tool) => (_jsx(SidebarItem, { icon: _jsx(tool.icon, { size: 13, "aria-hidden": true }), onPress: tool.go, children: tool.label }, tool.id))) })) : null] }) }) })), settled && onApex ? _jsx(Leave, { href: signedIn ? ENTRY : WORKSPACE, children: "Workspace" }) : null, settled ? (signedIn ? (_jsxs(_Fragment, { children: [superAdmin() ? (_jsx(SidebarItem, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), onPress: () => setSupporting(true), children: "All organizations" })) : null, _jsx(SidebarItem, { icon: _jsx(UserPlus, { size: 15, "aria-hidden": true }), onPress: () => window.open(invite(), "_blank", "noopener"), children: "Invite Teammates" }), _jsx(SidebarItem, { icon: _jsx(Share2, { size: 15, "aria-hidden": true }), onPress: () => {
1228
+ placeholder: "Filter", "aria-label": "Filter sidebar" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }) }), settingsPane ? (_jsx(SidebarScroll, { children: _jsx(SettingsList, { active: settingsPane, filter: filter, onPick: pick, onBack: () => showSettings(null) }) })) : (_jsx(_Fragment, { children: _jsx(SidebarScroll, { children: _jsxs(_Fragment, { children: [settled ? (_jsx(Index, { mode: mode, owner: owner, filter: filter, onPick: onPick, navigate: navigate })) : null, products.length ? (_jsx(SidebarFolder, { name: "Cloud & Platform", open: filter ? true : undefined, children: products.map((prod) => (_jsx(SidebarItem, { icon: _jsx(prod.icon, { size: 13, "aria-hidden": true }), onPress: () => window.open(prod.href, "_blank", "noopener"), children: prod.label }, prod.id))) })) : null] }) }) })), settled && onApex ? _jsx(Leave, { href: signedIn ? ENTRY : WORKSPACE, children: "Workspace" }) : null, settled ? (signedIn ? (_jsxs(_Fragment, { children: [superAdmin() ? (_jsx(SidebarItem, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), onPress: () => setSupporting(true), children: "All organizations" })) : null, _jsx(SidebarItem, { icon: _jsx(UserPlus, { size: 15, "aria-hidden": true }), onPress: () => window.open(invite(), "_blank", "noopener"), children: "Invite Teammates" }), _jsx(SidebarItem, { icon: _jsx(Share2, { size: 15, "aria-hidden": true }), onPress: () => {
1232
1229
  navigate?.(site("/referral"));
1233
1230
  dismiss();
1234
1231
  }, children: "Share Hanzo" }), _jsx(SidebarItem, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), onPress: () => window.open(BILLING, "_blank", "noopener"), children: "Upgrade" }), _jsx(Account, { onToggleCollapse: toggle, onProfile: () => setProfiling(true), onAppearance: () => showSettings("general"), onUsage: () => showSettings("usage"), onKeys: () => showSettings("keys") })] })) : null) : null] }));
package/dist/dev.js CHANGED
@@ -7,13 +7,19 @@
7
7
  // to leave. The name drawn at the top left is Hanzo Dev: this host is the
8
8
  // dev agent. Chat, on the rooms shell, is Hanzo AI.
9
9
  //
10
- // THE ADDRESS IS THE PATH. Dev is `/dev`, and what it shows is the path under
11
- // it in the builder's own grammar (lib/host.ts `app`, `under`), so a run, a
12
- // screen, a Settings section, a Customize tab, the plans and a project all
13
- // survive a reload, and Back steps through them. A move is a native history
14
- // entry, which the Next router folds into `usePathname`, so reading the address
15
- // and moving it are one path.
16
- import { useMemo } from 'react';
10
+ // THE ADDRESS IS A PATH. Dev is /dev, and every place the builder shows is a
11
+ // path under it in the builder's real-path grammar (lib/host.ts `app`, `place`;
12
+ // @hanzo/build/route `href`, `parse`): a run, a screen, a Settings section, a
13
+ // Customize tab, the plans and a project all survive a reload, and Back steps
14
+ // through them. The host hands the rooms the path (`<Rooms path>`); a move is a
15
+ // native history entry, which the Next router folds into `usePathname`, so
16
+ // reading the address and moving it are one path.
17
+ //
18
+ // A PANE THAT ASKS FOR SETTINGS (`showSettings`, open.ts) is answered here: the
19
+ // rooms' shell draws its own Settings pane, and the app has none, so Chat's
20
+ // "See usage" opens the builder's Usage — the page the account menu's Usage
21
+ // opens — rather than nothing.
22
+ import { useEffect, useMemo } from 'react';
17
23
  import { useLook, useRooms } from './host.js';
18
24
  import { useIam } from '@hanzo/iam/react';
19
25
  import { useSignOut } from './lib/signout.js';
@@ -21,8 +27,9 @@ import { administers, path, route } from '@hanzo/build';
21
27
  import { bearer, named, org, orgs, pick } from './lib/session.js';
22
28
  import { enter } from './lib/destination.js';
23
29
  import { api } from './lib/api.js';
24
- import { app, under } from './lib/host.js';
30
+ import { app, go, place } from './lib/host.js';
25
31
  import { link } from './lib/tags.js';
32
+ import { showSettings, useOpen } from './open.js';
26
33
  /** Where the builder links out: every one of them an address of this site. */
27
34
  const LINKS = {
28
35
  github: app('-/sync'),
@@ -32,15 +39,28 @@ const LINKS = {
32
39
  };
33
40
  /** The person's theme, when they have chosen one the builder can name. */
34
41
  const theme = (t) => (t === 'light' || t === 'dark' || t === 'system' ? t : undefined);
35
- /** The app address a builder path names, canonical; '' for anything that is not one. */
36
- const canonical = (at) => path(route(at ?? ''));
42
+ /** The builder address a pathname names: Dev's place, or New for a path that is not Dev's. */
43
+ const at = (pathname) => {
44
+ const p = pathname ? place(pathname) : null;
45
+ return p?.mode === 'dev' ? p.at : '';
46
+ };
37
47
  /** The host for a signed-in reader. */
38
48
  export function useDevHost() {
39
49
  const { user, accessToken } = useIam();
40
50
  const logout = useSignOut();
41
51
  const { router, path: pathname } = useRooms();
42
- const here = canonical(under(pathname));
52
+ const here = path(route(at(pathname)));
43
53
  const look = useLook();
54
+ const { settings } = useOpen();
55
+ // The builder names every Settings section the rooms' pane does that it has;
56
+ // one it does not have opens Settings itself.
57
+ useEffect(() => {
58
+ if (!settings)
59
+ return;
60
+ showSettings(null);
61
+ const asked = route(`-/settings/${settings}`);
62
+ go(app(path(asked.kind === 'settings' ? asked : { kind: 'settings', section: 'general' })));
63
+ }, [settings]);
44
64
  // The token's claims change only when the token does: a sign-in, a refresh.
45
65
  const memberships = useMemo(() => orgs(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
46
66
  const scoped = useMemo(() => org(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -68,19 +88,12 @@ export function useDevHost() {
68
88
  person: { name, email, avatar },
69
89
  admin: administers(bearer(), scoped),
70
90
  path: here,
71
- go: (to, how) => {
72
- const next = app(canonical(to));
73
- if (window.location.pathname === next && !window.location.search)
74
- return;
75
- if (how?.replace)
76
- window.history.replaceState(null, '', next);
77
- else
78
- window.history.pushState(null, '', next);
79
- },
91
+ go: (to, how) => go(app(to), how?.replace),
80
92
  links: LINKS,
81
- // An address of this site moves in the router; another origin is left in
82
- // this tab. platform.hanzo.ai is another product, and a click that names
83
- // it stays here — everything the builder had it for is in its Settings.
93
+ // An address of the app moves the app, another of this site moves in the
94
+ // router, and another origin is left in this tab. platform.hanzo.ai is
95
+ // another product, and a click that names it stays here — everything the
96
+ // builder had it for is in its Settings.
84
97
  open: (href) => {
85
98
  let url;
86
99
  try {
@@ -91,10 +104,13 @@ export function useDevHost() {
91
104
  }
92
105
  if (url.hostname === 'platform.hanzo.ai')
93
106
  return;
94
- if (url.origin === window.location.origin)
95
- router.push(`${url.pathname}${url.search}${url.hash}`);
96
- else
107
+ const to = `${url.pathname}${url.search}${url.hash}`;
108
+ if (url.origin !== window.location.origin)
97
109
  window.location.assign(link(url.href));
110
+ else if (place(url.pathname))
111
+ go(to);
112
+ else
113
+ router.push(to);
98
114
  },
99
115
  signIn: () => enter(),
100
116
  signOut: () => void logout(),
package/dist/host.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * configure({ site: 'https://hanzo.ai', home: '/', signUp: '/start' })
9
9
  *
10
- * <Rooms router={useRouter()} search={useSearchParams()} path={usePathname()} route={route} Link={Link}>
10
+ * <Rooms router={useRouter()} path={usePathname()} route={route} Link={Link}>
11
11
  * <Room mode="cal" />
12
12
  * </Rooms>
13
13
  *
@@ -17,7 +17,7 @@
17
17
  * any component and on pages the rooms never draw. So the host states them once
18
18
  * with `configure()`, in a module its root imports, before anything renders.
19
19
  *
20
- * The ROUTER, the query, the path and the route change as the page moves, so they ride
20
+ * The ROUTER, the path and the route change as the page moves, so they ride
21
21
  * React context, and a room reads them with `useRooms()`.
22
22
  */
23
23
  import { type AnchorHTMLAttributes, type ComponentType, type ReactNode } from 'react';
@@ -34,21 +34,17 @@ export interface Router {
34
34
  }
35
35
  interface Moving {
36
36
  router: Router;
37
- /** The address's query, for the rooms that read one (Chat's `?chat=`). */
38
- search: URLSearchParams | null;
39
37
  /** The address's path, for the rooms that read one (Dev's, under `/dev`). */
40
38
  path: string | null;
41
39
  route: string;
42
40
  Link: Link;
43
41
  }
44
- export interface RoomsProps extends Omit<Moving, 'search' | 'path'> {
45
- /** Absent where no mounted room reads the query, so a static export needs no Suspense for it. */
46
- search?: URLSearchParams | null;
42
+ export interface RoomsProps extends Omit<Moving, 'path'> {
47
43
  /** Absent where no mounted room reads the path. */
48
44
  path?: string | null;
49
45
  children?: ReactNode;
50
46
  }
51
- /** The router, the query, the route and the link the host mounted the rooms with. */
47
+ /** The router, the route and the link the host mounted the rooms with. */
52
48
  export declare const useRooms: () => Moving;
53
49
  /** The org in scope, by the id the appearance layers are keyed on. */
54
50
  export declare function useOrg(): {
@@ -76,5 +72,5 @@ export declare function useLook(): {
76
72
  * keeps their choice in step with IAM.
77
73
  */
78
74
  export declare function Look(): any;
79
- export declare function Rooms({ router, search, path, route, Link, children }: RoomsProps): import("react").JSX.Element;
75
+ export declare function Rooms({ router, path, route, Link, children }: RoomsProps): import("react").JSX.Element;
80
76
  export {};
package/dist/host.js CHANGED
@@ -9,7 +9,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  *
10
10
  * configure({ site: 'https://hanzo.ai', home: '/', signUp: '/start' })
11
11
  *
12
- * <Rooms router={useRouter()} search={useSearchParams()} path={usePathname()} route={route} Link={Link}>
12
+ * <Rooms router={useRouter()} path={usePathname()} route={route} Link={Link}>
13
13
  * <Room mode="cal" />
14
14
  * </Rooms>
15
15
  *
@@ -19,7 +19,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
19
19
  * any component and on pages the rooms never draw. So the host states them once
20
20
  * with `configure()`, in a module its root imports, before anything renders.
21
21
  *
22
- * The ROUTER, the query, the path and the route change as the page moves, so they ride
22
+ * The ROUTER, the path and the route change as the page moves, so they ride
23
23
  * React context, and a room reads them with `useRooms()`.
24
24
  */
25
25
  import { createContext, useContext } from 'react';
@@ -34,12 +34,11 @@ const Context = createContext({
34
34
  back: () => window.history.back(),
35
35
  forward: () => window.history.forward(),
36
36
  },
37
- search: null,
38
37
  path: null,
39
38
  route: '/',
40
39
  Link: Plain,
41
40
  });
42
- /** The router, the query, the route and the link the host mounted the rooms with. */
41
+ /** The router, the route and the link the host mounted the rooms with. */
43
42
  export const useRooms = () => useContext(Context);
44
43
  /** The org in scope, by the id the appearance layers are keyed on. */
45
44
  export function useOrg() {
@@ -72,6 +71,6 @@ export function Look() {
72
71
  useLook();
73
72
  return null;
74
73
  }
75
- export function Rooms({ router, search = null, path = null, route, Link, children }) {
76
- return _jsx(Context.Provider, { value: { router, search, path, route, Link }, children: children });
74
+ export function Rooms({ router, path = null, route, Link, children }) {
75
+ return _jsx(Context.Provider, { value: { router, path, route, Link }, children: children });
77
76
  }
@@ -19,10 +19,10 @@
19
19
  * person who cannot be sent back to their page must still be signed in, so a
20
20
  * refusal degrades to the default and never to an exception.
21
21
  */
22
- /** The default on the apex: the app, which is `/` for anyone signed in
23
- * (app/page.tsx). It answers the case where there is no page to go back to (a
24
- * typed /login, a /signup funnel, a stranger who cleared their storage
25
- * mid-flight). */
22
+ /** The default on the apex: `/`, which takes anyone signed in to the app, in
23
+ * the mode they were last in (lib/host.ts `resume`). It answers the case where
24
+ * there is no page to go back to (a typed /login, a /signup funnel, a stranger
25
+ * who cleared their storage mid-flight). */
26
26
  export declare const DEFAULT_DESTINATION = "/";
27
27
  /** Where a sign-in with no page to go back to lands on `hostname`: the app on
28
28
  * the apex, and the rooms' Home everywhere else. */
@@ -47,8 +47,8 @@ export declare function door(): string;
47
47
  /**
48
48
  * Send this reader to sign in, and back to this page after.
49
49
  *
50
- * Every signed-out door into the app is this one call: `/` at a place in the
51
- * app, /dev, a room. The page is written down, and the browser goes to `to`,
50
+ * Every signed-out door into the app is this one call: /chat, /dev and every
51
+ * path under them, a room. The page is written down, and the browser goes to `to`,
52
52
  * which asks IAM for an existing session before it draws a form
53
53
  * (components/auth/panel.tsx `Gate`). On a `guest` host the same address is asked for on hanzo.ai instead. With no `to`
54
54
  * the page is `door()`'s; a control that says "Sign in" names LOGIN.
@@ -22,10 +22,10 @@
22
22
  import { apex, guest, SIGNIN } from "./host.js";
23
23
  import { SHARED } from "./share.js";
24
24
  import { where } from "../where.js";
25
- /** The default on the apex: the app, which is `/` for anyone signed in
26
- * (app/page.tsx). It answers the case where there is no page to go back to (a
27
- * typed /login, a /signup funnel, a stranger who cleared their storage
28
- * mid-flight). */
25
+ /** The default on the apex: `/`, which takes anyone signed in to the app, in
26
+ * the mode they were last in (lib/host.ts `resume`). It answers the case where
27
+ * there is no page to go back to (a typed /login, a /signup funnel, a stranger
28
+ * who cleared their storage mid-flight). */
29
29
  export const DEFAULT_DESTINATION = "/";
30
30
  /** Where a sign-in with no page to go back to lands on `hostname`: the app on
31
31
  * the apex, and the rooms' Home everywhere else. */
@@ -107,8 +107,8 @@ export function door() {
107
107
  /**
108
108
  * Send this reader to sign in, and back to this page after.
109
109
  *
110
- * Every signed-out door into the app is this one call: `/` at a place in the
111
- * app, /dev, a room. The page is written down, and the browser goes to `to`,
110
+ * Every signed-out door into the app is this one call: /chat, /dev and every
111
+ * path under them, a room. The page is written down, and the browser goes to `to`,
112
112
  * which asks IAM for an existing session before it draws a form
113
113
  * (components/auth/panel.tsx `Gate`). On a `guest` host the same address is asked for on hanzo.ai instead. With no `to`
114
114
  * the page is `door()`'s; a control that says "Sign in" names LOGIN.
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Which host this export is read on, and what that host keeps.
2
+ * Which host this export is read on, what that host keeps, and the app's
3
+ * addresses — the one module that writes and reads them.
3
4
  *
4
5
  * hanzo.ai is the site for a stranger and the app for a member: the web app is
5
- * `/` once signed in (app/page.tsx), beside /chat. hanzo.team is the
6
- * workspace, its own app (hanzo-apps/team) with Home at its root. This export
7
- * still carries the rooms, so a room asked for on the apex is sent to the same
8
- * room on hanzo.team, and `output: 'export'` has no server to do that, so it is
9
- * decided in the browser off the hostname, here.
6
+ * /chat and /dev once signed in, and `/` takes a member to the mode they were
7
+ * last in. hanzo.team is the workspace, its own app (hanzo-apps/team) with Home
8
+ * at its root. This export still carries the rooms, so a room asked for on the
9
+ * apex is sent to the same room on hanzo.team, and `output: 'export'` has no
10
+ * server to do that, so it is decided in the browser off the hostname, here.
10
11
  *
11
12
  * Two predicates, one per product: `apex()` for the app, `team()` for the
12
13
  * workspace. Every host that is neither (localhost, a preview) draws the whole
@@ -30,37 +31,55 @@ export declare const ENTRY = "https://hanzo.team/?signin";
30
31
  */
31
32
  export declare const START = "https://hanzo.team/start";
32
33
  /**
33
- * The web app, for a signed-in reader, in two modes. Chat is the default, at
34
- * `/`. Dev is @hanzo/build's `<Builder>` at `/dev`, and what it shows is the
35
- * PATH under it, in the builder's own grammar (`route` in @hanzo/build): `/dev`
36
- * New, `/dev/sess_<id>` a run, `/dev/<org>/<repo>` a project,
37
- * `/dev/-/<screen>`, `/dev/-/settings[/<section>]`, `/dev/-/customize[/<tab>]`,
38
- * `/dev/<slug>` a deployed site. The export holds one page for all of them,
39
- * `/dev`, and the edge serves it for every path under it (hanzo.ai's
40
- * lib/edge.ts `APP`).
34
+ * THE APP'S ADDRESSES ARE PATHS, in two modes at two roots. Chat is /chat, a
35
+ * new conversation, and /chat/<id> one of the reader's. Dev is /dev, Dev's New,
36
+ * and every place of @hanzo/build's builder is a path under it, written and
37
+ * read by `href`/`parse` (@hanzo/build/route): /dev/run/<id>, /dev/projects,
38
+ * /dev/projects/<org>/<repo> a repository, /dev/projects/<slug> a deployed
39
+ * site, /dev/issues, /dev/artifacts, /dev/templates, /dev/automations[/<id>],
40
+ * /dev/machines, /dev/environments, /dev/customize[/<tab>],
41
+ * /dev/settings[/<section>], /dev/plans. The export holds one page per root,
42
+ * /chat and /dev, and the edge serves it for every path under it (hanzo.ai's
43
+ * lib/edge.ts `APP`), so every address reloads.
41
44
  */
45
+ export declare const CHAT = "/chat";
42
46
  export declare const DEV = "/dev";
43
- /** Dev at an app address; '' is Dev's New, `/dev` itself. */
44
- export declare const app: (path: string) => string;
45
- /** The app address a pathname names under `/dev`, or null for a pathname that is not Dev's. */
46
- export declare const under: (pathname: string | null | undefined) => string | null;
47
- /**
48
- * Chat, the app's default mode: `/` is a new conversation and `?chat=<id>` one
49
- * of the reader's. So the address says which mode the app is in — a path under
50
- * `/dev` for Dev, anything else Chat — and a reload keeps it.
51
- */
52
- export declare const TALK = "chat";
53
- /** `/` in Chat, on a conversation, or `/` itself for a new one. */
47
+ /** Chat on a conversation, or /chat itself for a new one. */
54
48
  export declare const talk: (id?: string | null) => string;
49
+ /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
50
+ export declare const app: (at: string) => string;
51
+ /** A place in the app: Chat on a conversation or none, or Dev at a builder address. */
52
+ export type Place = {
53
+ mode: 'chat';
54
+ thread: string | null;
55
+ } | {
56
+ mode: 'dev';
57
+ at: string;
58
+ };
59
+ /** The place a path names, or null when it names none of the app's. */
60
+ export declare function place(pathname: string): Place | null;
61
+ /** An address as analytics counts it: a record folded to its kind, so every conversation is one page. */
62
+ export declare function page(pathname: string): string;
55
63
  /**
56
- * Dev opened on one run, project or site, from anywhere: every "Try Hanzo Dev"
57
- * link, a room's Dev, the console's "open in the builder". It signs a stranger
58
- * in on the way (app/(app)/dev/_door.tsx), and is reached through `site()`:
59
- * here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value that is
60
- * exactly a session id, a repository or a site's slug is carried; anything else
61
- * opens /dev itself, so no caller can make this name another address.
64
+ * Dev opened on one run, repository or site, from anywhere: every "Try Hanzo
65
+ * Dev" link, a room's Dev, the console's "open in the builder". It signs a
66
+ * stranger in on the way (app/(app)/dev/_door.tsx), and is reached through
67
+ * `site()`: here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value
68
+ * that is exactly a session id, a repository or a site's slug is carried;
69
+ * anything else opens /dev itself, so no caller can make this name another
70
+ * address.
62
71
  */
63
72
  export declare const dev: (ref?: string | null) => string;
73
+ /**
74
+ * Move the app to an address of this page: a history entry, or in place of this
75
+ * one. No document load and no route change — the Next router folds a native
76
+ * history entry into `usePathname`, so the app follows the path it reads.
77
+ */
78
+ export declare function go(address: string, replace?: boolean): void;
79
+ /** Keep the mode the reader is in, for `resume`. */
80
+ export declare function remember(mode: Place['mode']): void;
81
+ /** Where `/` takes a member: the mode they were last in, Chat when none is kept. */
82
+ export declare function resume(): string;
64
83
  /**
65
84
  * The workspace's rooms: every route under app/(app) the apex does not keep.
66
85
  *