@hanzo/rooms 0.1.83 → 0.1.85

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/Support.js CHANGED
@@ -2,40 +2,20 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Support mode: a SuperAdmin steps into any organization to see what it sees.
4
4
  //
5
- // Everything here is IAM's. The list is IAM's organization list, which answers a
6
- // SuperAdmin every organization and anyone else only their own; the step in and
7
- // the step out are IAM's `assume` and `release`, which answer the operator's own
8
- // token re-scoped and record each step in the organization's audit log
9
- // (lib/auth/session.ts `support`). This file only draws them, and only for the
10
- // person IAM signs as a SuperAdmin (`superAdmin`); nothing here decides who may.
11
- import { useCallback, useEffect, useRef, useState } from 'react';
12
- import { Text, View, XStack, YStack } from '@hanzo/gui';
5
+ // Everything here is IAM's. The list of every organization is the chooser's
6
+ // (Orgs.tsx `Everyone`), IAM's organization list, which answers a SuperAdmin
7
+ // every organization and anyone else only their own; the step in and the step
8
+ // out are IAM's `assume` and `release`, which answer the operator's own token
9
+ // re-scoped and record each step in the organization's audit log
10
+ // (lib/session.ts `support`). This file draws the strip that says so while it
11
+ // lasts; nothing here decides who may.
12
+ import { useEffect, useState } from 'react';
13
+ import { Text, XStack } from '@hanzo/gui';
13
14
  import { Button } from '@hanzo/ui';
14
- import { X } from 'lucide-react';
15
15
  import { useHydrated } from './lib/hydrated.js';
16
- import { api } from './lib/api.js';
17
- import { abandon, assumed, bearer, named, support } from './lib/session.js';
18
- import { BAD, mix } from './lib/mix.js';
19
- /** The banner rides above the room's sheets, the picker between them. gui carries a `calc()` to the page unchanged;
20
- * its types only name custom properties. */
16
+ import { abandon, assumed, named, support } from './lib/session.js';
17
+ /** The banner rides above the room's sheets. gui carries a `calc()` to the page unchanged; its types only name custom properties. */
21
18
  const BANNER = 'calc(var(--z-modal) + 30)';
22
- const DIALOG = 'calc(var(--z-modal) + 20)';
23
- const SCRIM = mix('var(--pure-black)', 28, 'srgb');
24
- const muted = 'var(--muted-foreground)';
25
- /** A page of IAM's organization list: `q` narrows it, `cursor` continues it. */
26
- async function page(q, cursor) {
27
- const query = new URLSearchParams({ limit: '20' });
28
- if (q.trim())
29
- query.set('q', q.trim());
30
- if (cursor)
31
- query.set('cursor', cursor);
32
- const res = await fetch(`${api()}/v1/iam/organizations?${query}`, {
33
- headers: { Authorization: `Bearer ${bearer() ?? ''}` },
34
- });
35
- if (!res.ok)
36
- throw new Error(`IAM answered ${res.status}.`);
37
- return (await res.json());
38
- }
39
19
  /**
40
20
  * The strip across the top of every page while a SuperAdmin is inside an
41
21
  * organization that is not theirs: which one, as whom, and the way out. It reads
@@ -63,60 +43,3 @@ export function SupportBanner() {
63
43
  };
64
44
  return (_jsxs(XStack, { role: "status", "aria-label": "Support mode", position: "fixed", t: 52, l: "50%", x: "-50%", maxW: "calc(100vw - 32px)", z: BANNER, items: "center", justify: "center", gap: 12, flexWrap: "wrap", pt: 6, pr: 8, pb: 6, pl: 16, rounded: 999, bg: "var(--foreground)", children: [_jsxs(Text, { fontSize: "$2", color: "var(--background)", children: ["Support mode: you are in ", _jsx("strong", { children: org }), who ? ` as ${who}` : '', ". IAM records it under your name."] }), _jsx(Button, { size: "sm", onClick: leave, disabled: busy, children: busy ? 'Leaving' : 'Leave' })] }));
65
45
  }
66
- /**
67
- * Every organization, for a SuperAdmin to step into. The list and the search are
68
- * IAM's, a page at a time; picking one asks IAM to step in.
69
- */
70
- export function SupportPicker({ onClose }) {
71
- const [q, setQ] = useState('');
72
- const [rows, setRows] = useState([]);
73
- const [cursor, setCursor] = useState('');
74
- const [loading, setLoading] = useState(false);
75
- const [refused, setRefused] = useState(null);
76
- const [stepping, setStepping] = useState(null);
77
- // Only the newest read lands: a search typed while a page is in flight
78
- // supersedes it, so a slow answer to an old query never overwrites the new one.
79
- const asked = useRef(0);
80
- const load = useCallback(async (query, from) => {
81
- const mine = ++asked.current;
82
- setLoading(true);
83
- setRefused(null);
84
- try {
85
- const next = await page(query, from);
86
- if (mine !== asked.current)
87
- return;
88
- setRows((had) => {
89
- // IAM's cursor is a position, so an org created mid-walk can shift a row
90
- // across a page edge; each org is kept once.
91
- const all = from ? [...had, ...(next.organizations ?? [])] : next.organizations ?? [];
92
- return all.filter((o, i) => all.findIndex((x) => x.name === o.name) === i);
93
- });
94
- setCursor(next.cursor ?? '');
95
- }
96
- catch (e) {
97
- if (mine === asked.current)
98
- setRefused(e instanceof Error ? e.message : 'The list did not load.');
99
- }
100
- finally {
101
- if (mine === asked.current)
102
- setLoading(false);
103
- }
104
- }, []);
105
- useEffect(() => {
106
- const id = setTimeout(() => void load(q, ''), 250);
107
- return () => clearTimeout(id);
108
- }, [q, load]);
109
- const enter = (org) => {
110
- setStepping(org);
111
- setRefused(null);
112
- support(org).catch((e) => {
113
- setRefused(e instanceof Error ? e.message : 'IAM did not step in.');
114
- setStepping(null);
115
- });
116
- };
117
- return (_jsx(YStack, { role: "dialog", "aria-modal": true, "aria-label": "All organizations", onKeyDown: (e) => {
118
- // gui types the native event; on the web it is the DOM's, which carries the key.
119
- if ('key' in e && e.key === 'Escape')
120
- onClose();
121
- }, position: "fixed", inset: 0, z: DIALOG, bg: SCRIM, overflowY: "auto", py: "6vh", px: 16, onClick: onClose, children: _jsxs(YStack, { onClick: (e) => e.stopPropagation(), width: "100%", maxW: 560, mx: "auto", bg: "var(--background)", rounded: 16, pt: 24, px: 24, pb: 16, children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsx(Text, { render: "h2", fontSize: "$7", fontWeight: "500", color: "var(--text-primary)", children: "All organizations" }), _jsx(Text, { render: "p", mt: 6, fontSize: "$3", color: muted, lineHeight: 21, children: "Support mode. Step into any organization to see its chats, agents and billing as it does. IAM records every step under your name." })] }), _jsx(Button, { variant: "ghost", size: "icon-sm", mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsx(Text, { render: _jsx("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: "Find an organization", "aria-label": "Find an organization", autoFocus: true }), width: "100%", mt: 16, fontSize: "$3", px: 12, py: 10, rounded: 10, borderWidth: 1, borderColor: "var(--border)", bg: "var(--background)", color: "var(--foreground)" }), _jsxs(View, { role: "list", "aria-label": "Organizations", mt: 10, display: "grid", gap: 2, children: [rows.map((o) => (_jsxs(XStack, { render: "button", role: "listitem", onClick: () => enter(o.name), disabled: stepping !== null, items: "baseline", justify: "space-between", gap: 12, width: "100%", px: 12, py: 10, rounded: 10, borderWidth: 0, bg: "transparent", cursor: "pointer", children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", color: "var(--foreground)", text: "left", children: o.displayName || o.name }), _jsx(Text, { fontSize: "$2", color: muted, text: "left", children: stepping === o.name ? 'Stepping in' : o.name })] }, o.name))), !loading && rows.length === 0 && !refused ? (_jsx(Text, { render: "p", mx: 12, my: 8, fontSize: "$3", color: muted, children: "No organization matches." })) : null] }), refused ? (_jsx(Text, { render: "p", mx: 12, my: 8, fontSize: "$3", color: BAD, children: refused })) : null, cursor ? (_jsx(Button, { self: "flex-start", mt: 8, onClick: () => void load(q, cursor), disabled: loading, children: loading ? 'Loading' : 'More' })) : null] }) }));
122
- }
package/dist/agents.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Host } from '@hanzo/build';
1
2
  export type { Link, Shelf, ShelfRow as Row } from '@hanzo/build';
2
3
  /**
3
4
  * The model that draws an agent's face: Hanzo's own image SKU (hanzo-inc/zen
@@ -7,22 +8,20 @@ export type { Link, Shelf, ShelfRow as Row } from '@hanzo/build';
7
8
  export declare const PAINTER = "zen-image";
8
9
  /** The words every drawn face is asked for after its description. */
9
10
  export declare const MEMOJI = "memoji style 3D emoji portrait, head and shoulders, centered, plain background";
10
- /** A core member, drawn for an org that keeps no row of its own for them. */
11
- export interface House {
12
- id: string;
13
- name: string;
14
- avatar: string;
15
- }
16
- /** The default cast: the core team @hanzo/personas publishes, in the crew's order. */
17
- export declare const CORE: readonly House[];
18
11
  /**
19
- * The org's agents with the default cast filling in any member the org keeps no
20
- * row for, the cast first in its own order, then the rest as listed. With no
21
- * list yet (or none at all) it is the default cast.
12
+ * The default agents an org adds in one press (Customize › Agents, @hanzo/build
13
+ * `hire`): the core team, each as its persona writes it, then QA, Ops and
14
+ * Support. An org starts with none of them and loads none until it adds them.
15
+ */
16
+ export declare const CAST: NonNullable<Host['cast']>;
17
+ /**
18
+ * The org's own agents, the core team's first in the crew's order, then the
19
+ * rest as listed. Nothing stands in for a member the org has not added: an org
20
+ * with no agents has none to show.
22
21
  */
23
22
  export declare function ordered<T extends {
24
23
  name: string;
25
- }>(agents: readonly T[] | null | undefined): (T | House)[];
24
+ }>(agents: readonly T[] | null | undefined): T[];
26
25
  /**
27
26
  * One figure of @hanzo/personas' roster, as assets/personas/index.json lists it
28
27
  * (scripts/team.mjs). `called`, `about` and `based` are its PERSONA.md's
package/dist/agents.js CHANGED
@@ -10,20 +10,25 @@ import { TEAM } from './team.js';
10
10
  export const PAINTER = 'zen-image';
11
11
  /** The words every drawn face is asked for after its description. */
12
12
  export const MEMOJI = 'memoji style 3D emoji portrait, head and shoulders, centered, plain background';
13
- /** The default cast: the core team @hanzo/personas publishes, in the crew's order. */
14
- export const CORE = TEAM.map((one) => ({ id: `house-${one.id}`, name: one.name, avatar: `/agents/${one.id}.png` }));
13
+ /**
14
+ * The default agents an org adds in one press (Customize › Agents, @hanzo/build
15
+ * `hire`): the core team, each as its persona writes it, then QA, Ops and
16
+ * Support. An org starts with none of them and loads none until it adds them.
17
+ */
18
+ export const CAST = [
19
+ ...TEAM.map((one) => ({ name: one.id, description: `${one.name} — ${one.role}`, model: one.model, instructions: one.instructions, avatar: `/agents/${one.id}.png` })),
20
+ { name: 'qa', description: 'QA — Automated Testing & Verification', model: 'enso-auto', instructions: '' },
21
+ { name: 'ops', description: 'Ops — Site Reliability & Cloud Infrastructure', model: 'enso-auto', instructions: '' },
22
+ { name: 'support', description: 'Support — Customer Support & Triage Specialist', model: 'enso-auto', instructions: '' },
23
+ ];
15
24
  const RANK = new Map(TEAM.map((one, i) => [one.id, i]));
16
25
  /**
17
- * The org's agents with the default cast filling in any member the org keeps no
18
- * row for, the cast first in its own order, then the rest as listed. With no
19
- * list yet (or none at all) it is the default cast.
26
+ * The org's own agents, the core team's first in the crew's order, then the
27
+ * rest as listed. Nothing stands in for a member the org has not added: an org
28
+ * with no agents has none to show.
20
29
  */
21
30
  export function ordered(agents) {
22
31
  const list = Array.isArray(agents) ? [...agents] : [];
23
- const names = new Set(list.map((one) => one.name?.toLowerCase()));
24
- for (const one of CORE)
25
- if (!names.has(one.name.toLowerCase()))
26
- list.push(one);
27
32
  const rank = (one) => RANK.get(one.name?.toLowerCase() ?? '') ?? Infinity;
28
33
  return list.sort((a, b) => rank(a) - rank(b));
29
34
  }
package/dist/crew.js CHANGED
@@ -115,6 +115,7 @@ export function useAgentShelf() {
115
115
  loading,
116
116
  error: error ? reach(error) : null,
117
117
  search: 'Search agents',
118
+ empty: 'No agents yet. Add the default team from Manage agents.',
118
119
  create: { label: 'Create agent', onPress: () => make('new') },
119
120
  // Dev's Customize › Agents: a move inside the app where the host reads the
120
121
  // path (hanzo.ai), else Dev on hanzo.ai, as Chat's Create file or site goes.
package/dist/dev.js CHANGED
@@ -35,6 +35,7 @@ import { administers, path, route } from '@hanzo/build';
35
35
  import { bearer, named, org, orgs, pick } from './lib/session.js';
36
36
  import { enter } from './lib/destination.js';
37
37
  import { api } from './lib/api.js';
38
+ import { CAST } from './agents.js';
38
39
  import { app, go, place } from './lib/host.js';
39
40
  import { link } from './lib/tags.js';
40
41
  import { payUrl } from './lib/pay.js';
@@ -117,6 +118,9 @@ export function useDevHost() {
117
118
  path: here,
118
119
  go: (to, how) => (isPlans(to) ? leave('here') : go(app(to), how?.replace)),
119
120
  links: LINKS,
121
+ // The sidebar lists Settings' sections and Customize's tabs (Column.tsx), and Customize › Agents offers the cast.
122
+ sections: true,
123
+ cast: CAST,
120
124
  // An address of the app moves the app, another of this site moves in the
121
125
  // router, and another origin is left in this tab. platform.hanzo.ai is
122
126
  // another product, and a click that names it stays here — everything the
@@ -0,0 +1,29 @@
1
+ /**
2
+ * An organization's name as IAM keys it, said once for the chooser.
3
+ *
4
+ * IAM refuses a name that is not already canonical rather than rewriting it
5
+ * (hanzoai/iam pkg/schema/orgname.go `OrgName`: lowercasing "Admin" would make
6
+ * it the reserved org), so a slug typed here is checked and refused, never
7
+ * folded. What IS folded is a display name into the slug it suggests, the way
8
+ * IAM's own onboarding does it (internal/oidc/onboard.go `slugifyOrg`), so the
9
+ * slug shown is the slug IAM makes of the same name.
10
+ */
11
+ /** The longest organization name IAM takes. */
12
+ export declare const LONGEST = 55;
13
+ /**
14
+ * The slug IAM's onboarding makes of a name: lowercase ASCII letters and digits,
15
+ * every other run one hyphen, none at either end, at most `LONGEST` long.
16
+ */
17
+ export declare function slugify(name: string): string;
18
+ /**
19
+ * Why IAM would not take `slug` as an organization's name, or null when it
20
+ * would. A name onboarding keeps exactly as typed: IAM's shape with single inner
21
+ * hyphens, since onboarding folds a double one. The names IAM holds for the
22
+ * platform are IAM's to refuse, in its own words.
23
+ */
24
+ export declare function refusal(slug: string): string | null;
25
+ /** Whether an organization answers what was typed: its name or its display name, in any case (IAM's `matches`). */
26
+ export declare function matches(o: {
27
+ name: string;
28
+ display: string;
29
+ }, typed: string): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * An organization's name as IAM keys it, said once for the chooser.
3
+ *
4
+ * IAM refuses a name that is not already canonical rather than rewriting it
5
+ * (hanzoai/iam pkg/schema/orgname.go `OrgName`: lowercasing "Admin" would make
6
+ * it the reserved org), so a slug typed here is checked and refused, never
7
+ * folded. What IS folded is a display name into the slug it suggests, the way
8
+ * IAM's own onboarding does it (internal/oidc/onboard.go `slugifyOrg`), so the
9
+ * slug shown is the slug IAM makes of the same name.
10
+ */
11
+ /** The longest organization name IAM takes. */
12
+ export const LONGEST = 55;
13
+ /**
14
+ * The slug IAM's onboarding makes of a name: lowercase ASCII letters and digits,
15
+ * every other run one hyphen, none at either end, at most `LONGEST` long.
16
+ */
17
+ export function slugify(name) {
18
+ let out = '';
19
+ let dash = false;
20
+ for (const ch of name.toLowerCase()) {
21
+ if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
22
+ out += ch;
23
+ dash = false;
24
+ }
25
+ else if (!dash) {
26
+ out += '-';
27
+ dash = true;
28
+ }
29
+ }
30
+ return out.replace(/^-+|-+$/g, '').slice(0, LONGEST).replace(/-+$/, '');
31
+ }
32
+ /**
33
+ * Why IAM would not take `slug` as an organization's name, or null when it
34
+ * would. A name onboarding keeps exactly as typed: IAM's shape with single inner
35
+ * hyphens, since onboarding folds a double one. The names IAM holds for the
36
+ * platform are IAM's to refuse, in its own words.
37
+ */
38
+ export function refusal(slug) {
39
+ if (slug.length < 2)
40
+ return 'Use at least 2 letters or numbers.';
41
+ if (slug.length > LONGEST)
42
+ return `Use at most ${LONGEST} characters.`;
43
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))
44
+ return 'Use lowercase letters, digits and single hyphens between them.';
45
+ return null;
46
+ }
47
+ /** Whether an organization answers what was typed: its name or its display name, in any case (IAM's `matches`). */
48
+ export function matches(o, typed) {
49
+ const q = typed.trim().toLowerCase();
50
+ return !q || o.name.toLowerCase().includes(q) || o.display.toLowerCase().includes(q);
51
+ }
@@ -180,6 +180,10 @@ export declare const PICKED = "hanzo:org:picked";
180
180
  * app's account menu — is one the gate does not ask for again.
181
181
  */
182
182
  export declare function pick(o: string): void;
183
+ /** Fired on `window` to open the organization chooser over the room. */
184
+ export declare const CHOOSE = "hanzo:org:choose";
185
+ /** Open the chooser (`Orgs`): every organization of the person's, and every one for a SuperAdmin. */
186
+ export declare function choose(): void;
183
187
  /**
184
188
  * Whether the signed-in person is a SUPERADMIN: @hanzo/iam's `superAdmin`, which is
185
189
  * hanzoai/authz `Claims.Sudo` exactly — a person (never a program) whose own row's
@@ -437,6 +437,12 @@ export function pick(o) {
437
437
  }
438
438
  window.dispatchEvent(new Event(ORG));
439
439
  }
440
+ /** Fired on `window` to open the organization chooser over the room. */
441
+ export const CHOOSE = 'hanzo:org:choose';
442
+ /** Open the chooser (`Orgs`): every organization of the person's, and every one for a SuperAdmin. */
443
+ export function choose() {
444
+ window.dispatchEvent(new Event(CHOOSE));
445
+ }
440
446
  /**
441
447
  * Whether the signed-in person is a SUPERADMIN: @hanzo/iam's `superAdmin`, which is
442
448
  * hanzoai/authz `Claims.Sudo` exactly — a person (never a program) whose own row's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "peerDependencies": {
50
50
  "@hanzo/ai": ">=0.6.23",
51
51
  "@hanzo/appearance": ">=0.2.6",
52
- "@hanzo/build": ">=0.2.69",
52
+ "@hanzo/build": ">=0.2.73",
53
53
  "@hanzo/composer": ">=0.2.0",
54
54
  "@hanzo/event": ">=0.3.56",
55
55
  "@hanzo/gui": "^8.3.5",
@@ -67,7 +67,7 @@
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.23",
69
69
  "@hanzo/appearance": "0.2.8",
70
- "@hanzo/build": "0.2.69",
70
+ "@hanzo/build": "0.2.73",
71
71
  "@hanzo/composer": "0.2.0",
72
72
  "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",