@hanzo/build 0.2.70 → 0.2.73

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.
@@ -69,6 +69,46 @@ export declare const EMPTY: Draft;
69
69
  export declare function refuse(d: Draft): string;
70
70
  export declare function agents(t: Target): Promise<Agent[]>;
71
71
  export declare function one(t: Target, ref: string): Promise<Agent>;
72
+ /** One run of an agent, as its profile lists it: ok or error, what it was given and what it said. */
73
+ export interface Run {
74
+ id: string;
75
+ status: string;
76
+ model: string;
77
+ input: string;
78
+ output: string;
79
+ error: string;
80
+ ms: number;
81
+ at: string;
82
+ }
83
+ export declare function run(raw: unknown): Run;
84
+ /** An agent with its instructions and its 20 latest runs, newest first (`recentRuns`). */
85
+ export declare function profile(t: Target, ref: string): Promise<{
86
+ agent: Agent;
87
+ runs: Run[];
88
+ }>;
89
+ /** A default agent a host offers: a name, its line, its model and its instructions. */
90
+ export interface Cast {
91
+ name: string;
92
+ description: string;
93
+ model: string;
94
+ instructions: string;
95
+ /** Its face, an address of the host, when it has one. */
96
+ avatar?: string;
97
+ }
98
+ /** What a default agent may spend until its owner says otherwise, in dollars: each month, and one run. */
99
+ export declare const CAST_CAP = "10";
100
+ export declare const CAST_TASK = "1";
101
+ /** The cast members the org has no agent for, by name in any case. */
102
+ export declare const missing: (cast: readonly Cast[], have: readonly {
103
+ name: string;
104
+ }[]) => Cast[];
105
+ /** Create every cast member the org lacks, one at a time; the names made, and the first refusal, if any. */
106
+ export declare function hire(t: Target, cast: readonly Cast[], have: readonly {
107
+ name: string;
108
+ }[]): Promise<{
109
+ made: string[];
110
+ error: unknown;
111
+ }>;
72
112
  export declare function create(t: Target, d: Draft): Promise<Agent>;
73
113
  /** The fields that differ from `was`, and nothing else: an omitted field keeps its value. */
74
114
  export declare function changes(was: Agent, d: Draft): Record<string, unknown>;
package/lib/api/agents.js CHANGED
@@ -86,6 +86,37 @@ export async function agents(t) {
86
86
  export async function one(t, ref) {
87
87
  return agent(await call(t, 'GET', `/v1/agent/${seg(ref)}`));
88
88
  }
89
+ export function run(raw) {
90
+ const o = obj(raw);
91
+ return { id: str(o.id), status: str(o.status), model: str(o.model), input: str(o.input), output: str(o.output), error: str(o.error), ms: num(o.durationMs), at: str(o.createdAt) };
92
+ }
93
+ /** An agent with its instructions and its 20 latest runs, newest first (`recentRuns`). */
94
+ export async function profile(t, ref) {
95
+ const raw = await call(t, 'GET', `/v1/agent/${seg(ref)}`);
96
+ return { agent: agent(raw), runs: rows(obj(raw).recentRuns).map(run).filter((r) => r.id) };
97
+ }
98
+ /** What a default agent may spend until its owner says otherwise, in dollars: each month, and one run. */
99
+ export const CAST_CAP = '10';
100
+ export const CAST_TASK = '1';
101
+ /** The cast members the org has no agent for, by name in any case. */
102
+ export const missing = (cast, have) => {
103
+ const names = new Set(have.map((a) => a.name.toLowerCase()));
104
+ return cast.filter((c) => !names.has(c.name.toLowerCase()));
105
+ };
106
+ /** Create every cast member the org lacks, one at a time; the names made, and the first refusal, if any. */
107
+ export async function hire(t, cast, have) {
108
+ const made = [];
109
+ for (const c of missing(cast, have)) {
110
+ try {
111
+ await create(t, { ...EMPTY, name: c.name, description: c.description, model: c.model, instructions: c.instructions, cap: CAST_CAP, task: CAST_TASK, period: 'month' });
112
+ made.push(c.name);
113
+ }
114
+ catch (error) {
115
+ return { made, error };
116
+ }
117
+ }
118
+ return { made, error: null };
119
+ }
89
120
  export async function create(t, d) {
90
121
  const why = refuse(d);
91
122
  if (why)
package/lib/builder.js CHANGED
@@ -34,6 +34,7 @@ import { SessionRail } from '@hanzo/ui/chat';
34
34
  import { HanzoMark } from '@hanzo/ui/product';
35
35
  import { useMemo, useState } from 'react';
36
36
  import { safe } from './api/projects.js';
37
+ import { Profile } from './customize/agents.js';
37
38
  import { Customize } from './customize/index.js';
38
39
  import { Editor } from './automations.js';
39
40
  import { useKept, useRecents } from './data.js';
@@ -107,7 +108,9 @@ function Pane({ onStarted }) {
107
108
  if (r.kind === 'settings')
108
109
  return _jsx(Settings, { section: r.section });
109
110
  if (r.kind === 'customize')
110
- return _jsx(Customize, { tab: r.tab });
111
+ return _jsx(Customize, { tab: r.tab }, r.tab);
112
+ if (r.kind === 'agent')
113
+ return _jsx(Profile, { name: r.name }, r.name);
111
114
  if (r.kind === 'automation')
112
115
  return _jsx(Editor, { id: r.id }, r.id);
113
116
  if (r.kind === 'screen') {
package/lib/carry.d.ts CHANGED
@@ -39,4 +39,4 @@ export interface Asked {
39
39
  * `customize(tab)` opens Dev's Customize on that tab; `modes` are the ones this
40
40
  * surface offers (a choice it does not offer reads as Auto).
41
41
  */
42
- export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void, lent: Lent, modes: readonly Mode[]): Carry;
42
+ export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void, lent: Lent, modes?: readonly Mode[]): Carry;
package/lib/carry.js CHANGED
@@ -25,7 +25,7 @@ import { active, authored, document, nameOf as skillOf } from './api/skills.js';
25
25
  import { brand, brands } from './api/studio.js';
26
26
  import { useRead } from './data.js';
27
27
  import { said as designSaid } from './design.js';
28
- import { useGear } from './gear.js';
28
+ import { DEV_MODES, useGear } from './gear.js';
29
29
  import { active as lighted, prep, shelf as pluginShelf, usePlugins } from './plugins/index.js';
30
30
  /** The most of each skill a message carries. */
31
31
  const SKILL_MAX = 16_000;
@@ -72,7 +72,7 @@ function Allow({ what, onAnswer }) {
72
72
  * `customize(tab)` opens Dev's Customize on that tab; `modes` are the ones this
73
73
  * surface offers (a choice it does not offer reads as Auto).
74
74
  */
75
- export function useCarry(t, key, customize, lent, modes) {
75
+ export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
76
76
  const gear = useGear(t, key);
77
77
  const [dialog, show] = useState(null);
78
78
  // The composer a plugin acts on, read when it acts: the draft moves under it.
@@ -1,2 +1,6 @@
1
1
  import { type Pane } from './ui.tsx';
2
2
  export declare function Agents({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;
3
+ /** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
4
+ export declare function Profile({ name }: {
5
+ name: string;
6
+ }): import("react").JSX.Element;
@@ -5,21 +5,25 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  *
6
6
  * Browse is the platform's presets whose tool calls run on the platform;
7
7
  * starting from one opens a new agent written from it. Yours lists the org's
8
- * agents; opening one edits it in place, and only what changed is sent. An
8
+ * agents, and Add default agents makes every member of the host's cast the org
9
+ * has none for (an org starts with none). Opening one is its profile page
10
+ * (`-/customize/agents/<name>`): its face, part, model, instructions, tools,
11
+ * budget and runs, edited in place, and only what changed is sent. An
9
12
  * agent calls only the tools it names, and only those that are on for the org,
10
13
  * so the choices are the tools that are on; "every tool" is whatever the
11
14
  * fleet's MCP server serves when it runs. A budget is required: what it may
12
15
  * spend each period, and in one run.
13
16
  */
14
17
  import { SizableText, XStack, YStack } from '@hanzo/gui';
15
- import { Bot } from '@hanzogui/lucide-icons-2';
18
+ import { ArrowLeft, Bot, Users } from '@hanzogui/lucide-icons-2';
16
19
  import { Button, Input, Switch, Textarea } from '@hanzo/ui';
17
20
  import { ModelPicker } from '@hanzo/ui/models';
18
21
  import { useEffect, useMemo, useState } from 'react';
19
- import { agents, ALL, create, draft, EMPTY, fromPreset, one, PERIODS, presets, remove, update } from '../api/agents.js';
22
+ import { agents, ALL, create, draft, EMPTY, fromPreset, hire, missing, one, PERIODS, presets, profile, remove, update } from '../api/agents.js';
20
23
  import { models } from '../api/models.js';
21
24
  import { tools } from '../api/tools.js';
22
25
  import { useHost, useTarget } from '../host.js';
26
+ import { path } from '../route.js';
23
27
  import { useLoad } from './load.js';
24
28
  import { say } from './say.js';
25
29
  import { Add, Choice, Confirm, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
@@ -31,9 +35,21 @@ export function Agents({ view, q, adding, onAdding, onView, onCount }) {
31
35
  const mine = useLoad(signed ? () => agents(t) : null, [], [t, signed]);
32
36
  const offered = useLoad(view === 'discover' ? () => presets(t) : null, [], [t, view]);
33
37
  useCount(mine.value.length, onCount);
34
- const [opened, setOpened] = useState(null);
35
38
  const [start, setStart] = useState(null);
36
39
  const [note, setNote] = useState('');
40
+ const [hiring, setHiring] = useState(false);
41
+ const cast = host.cast ?? [];
42
+ const lacking = missing(cast, mine.value);
43
+ // The host's cast, made in one press: each member the org has no agent for.
44
+ const addCast = async () => {
45
+ setHiring(true);
46
+ setNote('');
47
+ const { made, error } = await hire(t, cast, mine.value);
48
+ setHiring(false);
49
+ mine.reload();
50
+ setNote(error ? `${made.length ? `Added ${made.length}. ` : ''}${say(error)}` : `Added ${made.length} default ${made.length === 1 ? 'agent' : 'agents'}`);
51
+ };
52
+ const castButton = signed && lacking.length ? (_jsxs(Button, { size: "sm", variant: "outline", disabled: hiring, onPress: () => void addCast(), children: [_jsx(Users, { size: 14 }), " ", hiring ? 'Adding…' : lacking.length === cast.length ? 'Add default agents' : `Add ${lacking.length} default ${lacking.length === 1 ? 'agent' : 'agents'}`] })) : null;
37
53
  const closeNew = () => {
38
54
  setStart(null);
39
55
  onAdding(false);
@@ -43,14 +59,6 @@ export function Agents({ view, q, adding, onAdding, onView, onCount }) {
43
59
  onView('yours');
44
60
  mine.reload();
45
61
  setNote(`${a.name} is saved`);
46
- } })) : opened ? (_jsx(Editor, { agent: opened, onClose: () => setOpened(null), onSaved: (a) => {
47
- setOpened(null);
48
- mine.reload();
49
- setNote(`${a.name} is saved`);
50
- }, onDeleted: () => {
51
- setNote(`${opened.name} is deleted`);
52
- setOpened(null);
53
- mine.reload();
54
62
  } })) : null;
55
63
  if (view === 'discover') {
56
64
  const names = new Set(mine.value.map((a) => a.name));
@@ -61,12 +69,30 @@ export function Agents({ view, q, adding, onAdding, onView, onCount }) {
61
69
  return _jsx(Visitor, { children: "Sign in to see your agents." });
62
70
  }
63
71
  const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
64
- return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail: "Start from a preset, or give a new one a model, instructions and tools.", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse presets" }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "New agent" })] })) : !shown.length ? (_jsx(Soft, { children: `No agent of yours matches “${q.trim()}”.` })) : (_jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Mark, { name: a.name, icon: a.emoji ? _jsx(SizableText, { size: "$4", children: a.emoji }) : undefined }), onOpen: () => setOpened(a) }, a.id || a.name))) })), editor] }));
65
- function reach(a) {
66
- if (a.tools.includes(ALL))
67
- return 'every tool';
68
- return a.tools.length === 1 ? '1 tool' : `${a.tools.length} tools`;
69
- }
72
+ return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail: cast.length ? 'Add the default team, start from a preset, or give a new one a model, instructions and tools.' : 'Start from a preset, or give a new one a model, instructions and tools.', children: [castButton, _jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse presets" }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "New agent" })] })) : !shown.length ? (_jsx(Soft, { children: `No agent of yours matches “${q.trim()}”.` })) : (_jsxs(_Fragment, { children: [castButton ? _jsx(XStack, { children: castButton }) : null, _jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Face, { agent: a, cast: cast }), onOpen: () => host.go(path({ kind: 'agent', name: a.name })) }, a.id || a.name))) })] })), editor] }));
73
+ }
74
+ function reach(a) {
75
+ if (a.tools.includes(ALL))
76
+ return 'every tool';
77
+ return a.tools.length === 1 ? '1 tool' : `${a.tools.length} tools`;
78
+ }
79
+ /** An agent's face: the cast member's picture, its emoji, or its initial. */
80
+ function Face({ agent: a, cast, size = 32 }) {
81
+ const avatar = cast.find((c) => c.name.toLowerCase() === a.name.toLowerCase())?.avatar;
82
+ return _jsx(Mark, { name: a.name, size: size, logo: avatar, icon: a.emoji ? _jsx(SizableText, { size: size > 40 ? '$8' : '$4', children: a.emoji }) : undefined });
83
+ }
84
+ /** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
85
+ export function Profile({ name }) {
86
+ const host = useHost();
87
+ const t = useTarget();
88
+ const got = useLoad(host.person ? () => profile(t, name) : null, null, [t, name]);
89
+ const [editing, setEditing] = useState(false);
90
+ const back = () => host.go(path({ kind: 'customize', tab: 'agents' }));
91
+ const a = got.value?.agent;
92
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 880, mx: "auto", gap: "$5", children: [_jsxs(XStack, { render: "button", onPress: back, items: "center", gap: "$1.5", self: "flex-start", cursor: "pointer", "aria-label": "Back to agents", children: [_jsx(ArrowLeft, { size: 14, color: "$soft" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Agents" })] }), !host.person ? (_jsx(Visitor, { children: "Sign in to see this agent." })) : got.error && !a ? (_jsx(Failed, { error: got.error, onRetry: got.reload })) : !a ? (_jsx(Soft, { children: "Loading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsxs(XStack, { items: "center", gap: "$4", children: [_jsx(Face, { agent: a, cast: host.cast ?? [], size: 64 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", numberOfLines: 1, children: a.name }), _jsx(SizableText, { size: "$3", color: "$soft", children: a.description || 'No role written yet.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditing(true), children: "Edit" })] }), _jsx(Part, { title: "Model", detail: a.model || 'The deployment’s default' }), _jsx(Part, { title: "Instructions", detail: a.instructions ? '' : 'None written: it runs on its model alone.', children: a.instructions ? (_jsx(YStack, { p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", maxH: 320, overflow: "scroll", children: _jsx(SizableText, { size: "$2", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: a.instructions }) })) : null }), _jsx(Part, { title: "Tools", detail: a.tools.includes(ALL) ? 'Every tool the fleet’s MCP server serves.' : a.tools.length ? a.tools.join(', ') : 'None: it calls no tool.' }), _jsx(Part, { title: "Budget", detail: a.cap ? `${money(a.spent)} of ${money(a.cap)} spent this ${a.period || 'period'}; at most ${money(a.task)} a run.` : 'No budget set.' }), _jsx(Part, { title: "Runs", detail: a.runs ? `${a.runs} in all; the latest ${Math.min(a.runs, got.value.runs.length)} below.` : 'It has not run yet.', children: got.value.runs.map((r) => (_jsxs(XStack, { gap: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", items: "flex-start", children: [_jsx(SizableText, { size: "$1", color: r.status === 'ok' ? '$soft' : '$bad', width: 44, shrink: 0, children: r.status === 'ok' ? 'ok' : 'failed' }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: r.input || '(no input)' }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: r.status === 'ok' ? r.output : r.error })] }), _jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: [r.model, r.at ? new Date(r.at).toLocaleString() : ''].filter(Boolean).join(' · ') })] }, r.id))) })] }))] }), editing && a ? (_jsx(Editor, { agent: a, onClose: () => setEditing(false), onSaved: () => {
93
+ setEditing(false);
94
+ got.reload();
95
+ }, onDeleted: back })) : null] }));
70
96
  }
71
97
  /** Write a new agent, or change one; an existing agent is read whole first, for its instructions. */
72
98
  function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
@@ -29,10 +29,11 @@ const LABEL = {
29
29
  plugins: { title: 'Plugins', says: 'Connectors you build in TypeScript.', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
30
30
  agents: { title: 'Agents', says: 'Agents with their own model, instructions and budget.', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
31
31
  };
32
+ /** Where a tab opens: an org's own agents (and the cast to add) for Agents; Browse for the rest, what there is to add being the page. */
33
+ const opens = (tab) => (tab === 'agents' ? 'yours' : 'discover');
32
34
  export function Customize({ tab, view: first }) {
33
35
  const host = useHost();
34
- // Browse first, always: a first visit has nothing of its own to show, and what there is to add is the page.
35
- const [view, setView] = useState(first ?? 'discover');
36
+ const [view, setView] = useState(first ?? opens(tab));
36
37
  const [q, setQ] = useState('');
37
38
  const [adding, setAdding] = useState(false);
38
39
  const [count, setCount] = useState(0);
@@ -40,17 +41,17 @@ export function Customize({ tab, view: first }) {
40
41
  const pick = (next) => {
41
42
  setQ('');
42
43
  setAdding(false);
43
- setView('discover');
44
+ setView(opens(next));
44
45
  setCount(0);
45
46
  host.go(path({ kind: 'customize', tab: next }));
46
47
  };
47
48
  const pane = { view, q, adding, onAdding: setAdding, onView: setView, onCount };
48
49
  const kit = ORG_KIT.includes(tab);
49
50
  const may = Boolean(host.person) && (host.admin || !kit);
50
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1080, mx: "auto", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: "Customize" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "What your agent can use in every run." })] }), _jsx(XStack, { role: "tablist", "aria-label": "Customize", gap: "$4", borderBottomWidth: 1, borderColor: "$borderColor", "$max-md": { gap: '$3' }, children: TABS.map((id) => {
51
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1080, mx: "auto", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: host.sections ? LABEL[tab].title : 'Customize' }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.sections ? LABEL[tab].says : 'What your agent can use in every run.' })] }), host.sections ? null : (_jsx(XStack, { role: "tablist", "aria-label": "Customize", gap: "$4", borderBottomWidth: 1, borderColor: "$borderColor", "$max-md": { gap: '$3' }, children: TABS.map((id) => {
51
52
  const on = id === tab;
52
53
  return (_jsxs(XStack, { role: "tab", render: "button", "aria-selected": on, onPress: () => pick(id), items: "center", gap: "$1.5", pb: "$2", mb: -1, borderBottomWidth: 2, borderColor: on ? '$ink' : 'transparent', hoverStyle: { borderColor: on ? '$ink' : '$edge' }, children: [_jsx(XStack, { opacity: on ? 1 : 0.6, "$max-md": { display: 'none' }, children: LABEL[id].icon }), _jsx(SizableText, { size: "$3", color: on ? '$ink' : '$soft', children: LABEL[id].title })] }, id));
53
- }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: LABEL[tab].says }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Choice, { label: "Show", value: view, options: [
54
+ }) })), host.sections ? null : (_jsx(SizableText, { size: "$2", color: "$soft", children: LABEL[tab].says })), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Choice, { label: "Show", value: view, options: [
54
55
  { id: 'discover', label: 'Browse' },
55
56
  { id: 'yours', label: 'Yours', count },
56
57
  ], onChange: setView }), _jsx(XStack, { flex: 1 }), may ? (_jsxs(Button, { size: "sm", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", LABEL[tab].add] })) : null] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: `${LABEL[tab].find}…`, "aria-label": LABEL[tab].find }), host.person && kit && !host.admin ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Only an org admin can add or change ", LABEL[tab].title.toLowerCase(), ". You can see what is on."] })) : null, tab === 'skills' ? (_jsx(Skills, { ...pane })) : tab === 'connectors' ? (_jsx(Connectors, { ...pane })) : tab === 'plugins' ? (_jsx(Plugins, { ...pane })) : (_jsx(Agents, { ...pane }))] }) }));
package/lib/host.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * as values, so no host forks the builder and the builder imports no host.
9
9
  */
10
10
  import { type ComponentType, type ReactNode } from 'react';
11
+ import type { Cast } from './api/agents.ts';
11
12
  import type { Target } from './api/call.ts';
12
13
  import type { Theme } from './api/pref.ts';
13
14
  export interface Person {
@@ -104,6 +105,10 @@ export interface Host {
104
105
  browse?: Browse;
105
106
  /** What the host's browser does to a page, row by row in the tab's menu. */
106
107
  browser?: Browser;
108
+ /** The host's sidebar lists Settings' sections and Customize's tabs, so the pages draw neither. */
109
+ sections?: boolean;
110
+ /** The default agents an org may add in one press (Customize › Agents). Absent, none are offered. */
111
+ cast?: readonly Cast[];
107
112
  }
108
113
  export declare function HostProvider({ host, children }: {
109
114
  host: Host;
package/lib/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export { Library, Outputs, Preview, Thumb, want } from './library.tsx';
25
25
  export { SOURCE, type Item, type Kind as ItemKind, type Source } from './api/library.ts';
26
26
  export { Scheduled, schedule } from './scheduled.tsx';
27
27
  export { Editor } from './automations.tsx';
28
- export { nav, NEW, type Group } from './nav.tsx';
28
+ export { nav, NEW, own, sections, tabs, type Group } from './nav.tsx';
29
29
  export { DevSection, DOTS, useSessions } from './section.tsx';
30
30
  export { HostProvider } from './host.tsx';
31
31
  export { Slack, useWho, Who } from './foot.tsx';
package/lib/index.js CHANGED
@@ -25,7 +25,7 @@ export { Library, Outputs, Preview, Thumb, want } from './library.js';
25
25
  export { SOURCE } from './api/library.js';
26
26
  export { Scheduled, schedule } from './scheduled.js';
27
27
  export { Editor } from './automations.js';
28
- export { nav, NEW } from './nav.js';
28
+ export { nav, NEW, own, sections, tabs } from './nav.js';
29
29
  export { DevSection, DOTS, useSessions } from './section.js';
30
30
  export { HostProvider } from './host.js';
31
31
  export { Slack, useWho, Who } from './foot.js';
package/lib/nav.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  import type { RailLink } from '@hanzo/ui/chat';
2
2
  import type { Host } from './host.tsx';
3
- /** A run of places under one quiet label. */
3
+ /** A run of places under one quiet label; '' draws none. */
4
4
  export interface Group {
5
- id: 'work' | 'make' | 'run' | 'setup';
5
+ id: string;
6
6
  label: string;
7
7
  links: RailLink[];
8
8
  }
9
9
  /** The id of the row that starts a new run: a rail with a New control of its own draws that instead. */
10
10
  export declare const NEW = "new";
11
11
  export declare function nav(host: Host, go: (path: string) => void): Group[];
12
+ /** Dev's own places: what a host whose rail holds the rest (Library, Scheduled tasks, Customize) lists in Dev's sidebar. */
13
+ export declare const own: (host: Host, go: (path: string) => void) => Group[];
14
+ /**
15
+ * Settings' sections as a host's sidebar lists them: one group per part, a
16
+ * part's label drawn only where it is not its first row's name (Code leads
17
+ * Code's part, so the part is not labelled twice).
18
+ */
19
+ export declare function sections(host: Host, go: (path: string) => void): Group[];
20
+ /** Customize's tabs as a host's sidebar lists them, Agents first; an agent's profile lights Agents. */
21
+ export declare function tabs(host: Host, go: (path: string) => void): Group[];
package/lib/nav.js CHANGED
@@ -9,10 +9,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  * Every row moves the builder through `go` with the address `path` writes, so
10
10
  * where a place lives is said here and in route.ts and nowhere else.
11
11
  */
12
- import { BookOpen, CircleDot, Clock, Container, Cpu, FolderGit2, LayoutTemplate, Library, SlidersHorizontal, SquarePen } from '@hanzogui/lucide-icons-2';
12
+ import { Bell, BookOpen, Bot, Brain, CircleDot, Clock, Code, Container, Cpu, CreditCard, FolderGit2, Gauge, Key, LayoutTemplate, Library, Plug, Puzzle, Settings, Shield, SlidersHorizontal, Sparkles, SquarePen, User, Users } from '@hanzogui/lucide-icons-2';
13
13
  import { pinBoard } from './choice.js';
14
14
  import { DOCS } from './foot.js';
15
- import { path, route } from './route.js';
15
+ import { path, route, TABS } from './route.js';
16
+ import { GROUPS, heading, LIST } from './settings/list.js';
16
17
  /** The id of the row that starts a new run: a rail with a New control of its own draws that instead. */
17
18
  export const NEW = 'new';
18
19
  export function nav(host, go) {
@@ -44,6 +45,7 @@ export function nav(host, go) {
44
45
  },
45
46
  active: screen === 'issues',
46
47
  },
48
+ { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
47
49
  ],
48
50
  },
49
51
  {
@@ -51,7 +53,6 @@ export function nav(host, go) {
51
53
  label: 'Make',
52
54
  links: [
53
55
  { id: 'artifacts', label: 'Library', icon: _jsx(Library, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
54
- { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
55
56
  { id: 'automations', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' || r.kind === 'automation' },
56
57
  ],
57
58
  },
@@ -67,9 +68,52 @@ export function nav(host, go) {
67
68
  id: 'setup',
68
69
  label: 'Setup',
69
70
  links: [
70
- { id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: to(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
71
+ { id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: to(path({ kind: 'customize', tab: 'agents' })), active: r.kind === 'customize' || r.kind === 'agent' || screen === 'mcp' },
71
72
  { id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
72
73
  ],
73
74
  },
74
75
  ];
75
76
  }
77
+ /** Dev's own places: what a host whose rail holds the rest (Library, Scheduled tasks, Customize) lists in Dev's sidebar. */
78
+ export const own = (host, go) => nav(host, go).filter((g) => g.id === 'work');
79
+ /**
80
+ * Settings' sections as a host's sidebar lists them: one group per part, a
81
+ * part's label drawn only where it is not its first row's name (Code leads
82
+ * Code's part, so the part is not labelled twice).
83
+ */
84
+ export function sections(host, go) {
85
+ const r = route(host.path);
86
+ const at = r.kind === 'settings' ? r.section : '';
87
+ return GROUPS.map((g) => {
88
+ const rows = LIST.filter((e) => e.group === g);
89
+ return {
90
+ id: g,
91
+ label: heading(g),
92
+ links: rows.map((e) => ({ id: e.id, label: e.label, icon: SECTION_ICON[e.id], onPress: () => go(path({ kind: 'settings', section: e.id })), active: e.id === at })),
93
+ };
94
+ });
95
+ }
96
+ /** Customize's tabs as a host's sidebar lists them, Agents first; an agent's profile lights Agents. */
97
+ export function tabs(host, go) {
98
+ const r = route(host.path);
99
+ const at = r.kind === 'customize' ? r.tab : r.kind === 'agent' ? 'agents' : '';
100
+ return [{ id: 'customize', label: '', links: TABS.map((t) => ({ id: t, label: TAB_LABEL[t], icon: TAB_ICON[t], onPress: () => go(path({ kind: 'customize', tab: t })), active: t === at })) }];
101
+ }
102
+ const TAB_LABEL = { agents: 'Agents', skills: 'Skills', connectors: 'Connectors', plugins: 'Plugins' };
103
+ const TAB_ICON = { agents: _jsx(Bot, { size: 16 }), skills: _jsx(BookOpen, { size: 16 }), connectors: _jsx(Plug, { size: 16 }), plugins: _jsx(Puzzle, { size: 16 }) };
104
+ const SECTION_ICON = {
105
+ general: _jsx(Settings, { size: 16 }),
106
+ account: _jsx(User, { size: 16 }),
107
+ privacy: _jsx(Shield, { size: 16 }),
108
+ billing: _jsx(CreditCard, { size: 16 }),
109
+ usage: _jsx(Gauge, { size: 16 }),
110
+ capabilities: _jsx(Sparkles, { size: 16 }),
111
+ memory: _jsx(Brain, { size: 16 }),
112
+ code: _jsx(Code, { size: 16 }),
113
+ environments: _jsx(Container, { size: 16 }),
114
+ machines: _jsx(Cpu, { size: 16 }),
115
+ keys: _jsx(Key, { size: 16 }),
116
+ members: _jsx(Users, { size: 16 }),
117
+ integrations: _jsx(Plug, { size: 16 }),
118
+ notifications: _jsx(Bell, { size: 16 }),
119
+ };
package/lib/route.d.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  * -/<screen> a builder screen
7
7
  * -/automations/<id> one automation's editor and runs; `new` writes one
8
8
  * -/settings[/<section>] a section of Settings
9
- * -/customize[/<tab>] skills, connectors and plugins: what the agent brings to a run
9
+ * -/customize[/<tab>] agents, skills, connectors and plugins: what the agent brings to a run
10
+ * -/customize/agents/<name> one agent's profile
10
11
  * -/plans the plans an organization can be on
11
12
  * <org>/<repo> a repository's workspace: a project is a repository on the forge
12
13
  * <slug> a deployed site's workspace (Artifacts)
@@ -30,8 +31,8 @@ export type Screen = 'artifacts' | 'templates' | 'projects' | 'issues' | 'automa
30
31
  /** Settings, one section each. Every setting the builder has lives on this page, none elsewhere. */
31
32
  export declare const SECTIONS: readonly ['general', 'account', 'privacy', 'billing', 'usage', 'capabilities', 'memory', 'code', 'environments', 'machines', 'keys', 'members', 'integrations', 'notifications'];
32
33
  export type Section = (typeof SECTIONS)[number];
33
- /** Customize, one tab each: what the agent brings to a run, yours and to discover. */
34
- export declare const TABS: readonly ['skills', 'connectors', 'plugins', 'agents'];
34
+ /** Customize, one tab each: what the agent brings to a run, yours and to discover. Agents first. */
35
+ export declare const TABS: readonly ['agents', 'skills', 'connectors', 'plugins'];
35
36
  export type Tab = (typeof TABS)[number];
36
37
  export type Route = {
37
38
  kind: 'new';
@@ -47,6 +48,9 @@ export type Route = {
47
48
  } | {
48
49
  kind: 'customize';
49
50
  tab: Tab;
51
+ } | {
52
+ kind: 'agent';
53
+ name: string;
50
54
  } | {
51
55
  kind: 'project';
52
56
  slug: string;
package/lib/route.js CHANGED
@@ -15,8 +15,8 @@ export const SECTIONS = [
15
15
  'integrations',
16
16
  'notifications',
17
17
  ];
18
- /** Customize, one tab each: what the agent brings to a run, yours and to discover. */
19
- export const TABS = ['skills', 'connectors', 'plugins', 'agents'];
18
+ /** Customize, one tab each: what the agent brings to a run, yours and to discover. Agents first. */
19
+ export const TABS = ['agents', 'skills', 'connectors', 'plugins'];
20
20
  export const SESSION = /^sess_[0-9a-f]{32}$/;
21
21
  export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
22
22
  /** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
@@ -32,8 +32,12 @@ export function route(path) {
32
32
  const section = (p.slice('-/settings/'.length) || 'general');
33
33
  return SECTIONS.includes(section) ? { kind: 'settings', section } : { kind: 'new' };
34
34
  }
35
+ if (p.startsWith('-/customize/agents/')) {
36
+ const name = p.slice('-/customize/agents/'.length);
37
+ return NAME.test(name) ? { kind: 'agent', name } : { kind: 'new' };
38
+ }
35
39
  if (p === '-/customize' || p.startsWith('-/customize/')) {
36
- const tab = (p.slice('-/customize/'.length) || 'skills');
40
+ const tab = (p.slice('-/customize/'.length) || 'agents');
37
41
  return TABS.includes(tab) ? { kind: 'customize', tab } : { kind: 'new' };
38
42
  }
39
43
  if (p.startsWith('-/automations/')) {
@@ -103,7 +107,9 @@ export function path(r) {
103
107
  case 'settings':
104
108
  return r.section === 'general' ? '-/settings' : `-/settings/${r.section}`;
105
109
  case 'customize':
106
- return r.tab === 'skills' ? '-/customize' : `-/customize/${r.tab}`;
110
+ return r.tab === 'agents' ? '-/customize' : `-/customize/${r.tab}`;
111
+ case 'agent':
112
+ return `-/customize/agents/${r.name}`;
107
113
  case 'project':
108
114
  return r.slug;
109
115
  case 'repo':
@@ -8,14 +8,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  import { SizableText, XStack, YStack } from '@hanzo/gui';
9
9
  import { useHost } from '../host.js';
10
10
  import { path } from '../route.js';
11
+ import { heading } from './list.js';
11
12
  import { ENTRIES, GROUPS } from './sections.js';
12
13
  export function Settings({ section }) {
13
14
  const host = useHost();
14
15
  // Every section the route grammar names has an entry.
15
16
  const entry = ENTRIES.find((e) => e.id === section);
16
17
  const go = (id) => host.go(path({ kind: 'settings', section: id }));
17
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [_jsxs(YStack, { role: "navigation", "aria-label": "Settings", width: 220, shrink: 0, borderRightWidth: 1, borderColor: "$borderColor", px: "$2", py: "$4", gap: "$4", overflow: "scroll", "$max-md": { display: 'none' }, children: [_jsx(SizableText, { size: "$4", color: "$ink", px: "$2", children: "Settings" }), GROUPS.map((g) => {
18
+ return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [host.sections ? null : (_jsxs(YStack, { role: "navigation", "aria-label": "Settings", width: 220, shrink: 0, borderRightWidth: 1, borderColor: "$borderColor", px: "$2", py: "$4", gap: "$4", overflow: "scroll", "$max-md": { display: 'none' }, children: [_jsx(SizableText, { size: "$4", color: "$ink", px: "$2", children: "Settings" }), GROUPS.map((g) => {
18
19
  const list = ENTRIES.filter((e) => e.group === g);
19
- return (_jsxs(YStack, { gap: "$0.5", children: [g === 'Settings' ? null : (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g })), list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
20
- })] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
20
+ return (_jsxs(YStack, { gap: "$0.5", children: [heading(g) ? (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g })) : null, list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
21
+ })] })), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
21
22
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The sections of Settings, in the order the page lists them, by name and part
3
+ * alone: what a sidebar lists without loading the sections themselves
4
+ * (sections.tsx pairs each with its body).
5
+ */
6
+ import type { Section } from '../route.ts';
7
+ export type Group = 'Settings' | 'Code' | 'Organization';
8
+ export declare const GROUPS: readonly Group[];
9
+ export declare const LIST: readonly {
10
+ id: Section;
11
+ label: string;
12
+ group: Group;
13
+ }[];
14
+ /** A part's label as a list draws it: none for the first part, or where the part's first row already says it (Code). */
15
+ export declare const heading: (g: Group) => string;
@@ -0,0 +1,19 @@
1
+ export const GROUPS = ['Settings', 'Code', 'Organization'];
2
+ export const LIST = [
3
+ { id: 'general', label: 'General', group: 'Settings' },
4
+ { id: 'account', label: 'Account', group: 'Settings' },
5
+ { id: 'privacy', label: 'Privacy', group: 'Settings' },
6
+ { id: 'billing', label: 'Billing', group: 'Settings' },
7
+ { id: 'usage', label: 'Usage', group: 'Settings' },
8
+ { id: 'capabilities', label: 'Capabilities', group: 'Settings' },
9
+ { id: 'memory', label: 'Memory', group: 'Settings' },
10
+ { id: 'code', label: 'Code', group: 'Code' },
11
+ { id: 'environments', label: 'Environments', group: 'Code' },
12
+ { id: 'machines', label: 'Machines', group: 'Code' },
13
+ { id: 'keys', label: 'API keys', group: 'Code' },
14
+ { id: 'members', label: 'Members', group: 'Organization' },
15
+ { id: 'integrations', label: 'Integrations', group: 'Organization' },
16
+ { id: 'notifications', label: 'Notifications', group: 'Organization' },
17
+ ];
18
+ /** A part's label as a list draws it: none for the first part, or where the part's first row already says it (Code). */
19
+ export const heading = (g) => (g === 'Settings' || LIST.find((e) => e.group === g)?.label === g ? '' : g);
@@ -5,8 +5,8 @@
5
5
  */
6
6
  import type { ReactNode } from 'react';
7
7
  import type { Section } from '../route.ts';
8
- export type Group = 'Settings' | 'Code' | 'Organization';
9
- export declare const GROUPS: readonly Group[];
8
+ import { type Group } from './list.ts';
9
+ export { GROUPS, type Group } from './list.ts';
10
10
  export interface Entry {
11
11
  id: Section;
12
12
  label: string;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { LIST } from './list.js';
2
3
  import { Account } from './account.js';
3
4
  import { Billing } from './billing.js';
4
5
  import { Capabilities } from './capabilities.js';
@@ -13,20 +14,21 @@ import { Memory } from './memory.js';
13
14
  import { Notifications } from './notifications.js';
14
15
  import { Privacy } from './privacy.js';
15
16
  import { Usage } from './usage.js';
16
- export const GROUPS = ['Settings', 'Code', 'Organization'];
17
- export const ENTRIES = [
18
- { id: 'general', label: 'General', group: 'Settings', body: () => _jsx(General, {}) },
19
- { id: 'account', label: 'Account', group: 'Settings', body: () => _jsx(Account, {}) },
20
- { id: 'privacy', label: 'Privacy', group: 'Settings', body: () => _jsx(Privacy, {}) },
21
- { id: 'billing', label: 'Billing', group: 'Settings', body: () => _jsx(Billing, {}) },
22
- { id: 'usage', label: 'Usage', group: 'Settings', body: () => _jsx(Usage, {}) },
23
- { id: 'capabilities', label: 'Capabilities', group: 'Settings', body: () => _jsx(Capabilities, {}) },
24
- { id: 'memory', label: 'Memory', group: 'Settings', body: () => _jsx(Memory, {}) },
25
- { id: 'code', label: 'Code', group: 'Code', body: () => _jsx(Code, {}) },
26
- { id: 'environments', label: 'Environments', group: 'Code', body: () => _jsx(Environments, {}) },
27
- { id: 'machines', label: 'Machines', group: 'Code', body: () => _jsx(Machines, {}) },
28
- { id: 'keys', label: 'API keys', group: 'Code', body: () => _jsx(Keys, {}) },
29
- { id: 'members', label: 'Members', group: 'Organization', body: () => _jsx(Members, {}) },
30
- { id: 'integrations', label: 'Integrations', group: 'Organization', body: () => _jsx(Integrations, {}) },
31
- { id: 'notifications', label: 'Notifications', group: 'Organization', body: () => _jsx(Notifications, {}) },
32
- ];
17
+ export { GROUPS } from './list.js';
18
+ const BODY = {
19
+ general: () => _jsx(General, {}),
20
+ account: () => _jsx(Account, {}),
21
+ privacy: () => _jsx(Privacy, {}),
22
+ billing: () => _jsx(Billing, {}),
23
+ usage: () => _jsx(Usage, {}),
24
+ capabilities: () => _jsx(Capabilities, {}),
25
+ memory: () => _jsx(Memory, {}),
26
+ code: () => _jsx(Code, {}),
27
+ environments: () => _jsx(Environments, {}),
28
+ machines: () => _jsx(Machines, {}),
29
+ keys: () => _jsx(Keys, {}),
30
+ members: () => _jsx(Members, {}),
31
+ integrations: () => _jsx(Integrations, {}),
32
+ notifications: () => _jsx(Notifications, {}),
33
+ };
34
+ export const ENTRIES = LIST.map((e) => ({ ...e, body: BODY[e.id] }));
package/lib/summary.js CHANGED
@@ -12,18 +12,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
12
12
  */
13
13
  import { useMemo, useState } from 'react';
14
14
  import { SizableText, XStack, YStack } from '@hanzo/gui';
15
- import { ListTodo } from '@hanzogui/lucide-icons-2';
15
+ import { FilePlus2, ListTodo } from '@hanzogui/lucide-icons-2';
16
16
  import { Popover, PopoverContent, PopoverTrigger } from '@hanzo/ui';
17
+ /** A row's pitch and inset, px: the kit menu's, so this panel reads as every other one. */
18
+ const ROW_H = 32;
19
+ const ROW_PX = 8;
17
20
  /** A row of the summary: its mark and its name, one press from a tab. */
18
21
  function Line({ icon, children, detail, onPress, quiet = false }) {
19
- return (_jsxs(XStack, { render: onPress ? 'button' : undefined, role: onPress ? 'button' : undefined, onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 30, width: "100%", rounded: "$3", cursor: onPress ? 'pointer' : undefined, hoverStyle: onPress ? { bg: '$hover' } : undefined, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [icon ? (_jsx(XStack, { width: 16, justify: "center", shrink: 0, opacity: 0.75, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, py: detail ? '$1' : 0, children: [_jsx(SizableText, { fontSize: 13, color: quiet ? '$soft' : '$ink', numberOfLines: 1, children: children }), detail ? (_jsx(SizableText, { fontSize: 12, color: "$soft", numberOfLines: 2, children: detail })) : null] })] }));
22
+ return (_jsxs(XStack, { render: onPress ? 'button' : undefined, role: onPress ? 'button' : undefined, onPress: onPress, items: "center", justify: "flex-start", gap: ROW_PX, px: ROW_PX, minH: ROW_H, width: "100%", rounded: 4, cursor: onPress ? 'pointer' : undefined, hoverStyle: onPress ? { bg: '$hover' } : undefined, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [icon ? (_jsx(XStack, { width: 16, justify: "center", shrink: 0, opacity: 0.75, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", py: detail ? '$1' : 0, children: [_jsx(SizableText, { fontSize: 14, color: quiet ? '$soft' : '$ink', numberOfLines: 1, style: { textAlign: 'left' }, children: children }), detail ? (_jsx(SizableText, { fontSize: 12, color: "$soft", numberOfLines: 2, style: { textAlign: 'left' }, children: detail })) : null] })] }));
20
23
  }
21
24
  /** A section's heading, with what it adds at its end. */
22
- const Head = ({ children, action }) => (_jsxs(XStack, { items: "center", justify: "space-between", px: "$2", pt: "$1.5", pb: "$1", minH: 28, children: [_jsx(SizableText, { fontSize: 13, color: "$soft", children: children }), action] }));
25
+ const Head = ({ children, action }) => (_jsxs(XStack, { items: "center", justify: "space-between", px: ROW_PX, minH: ROW_H, children: [_jsx(SizableText, { fontSize: 13, color: "$soft", children: children }), action] }));
23
26
  /** One section's rows; `done` runs after a row is pressed (the popover puts itself away). */
24
27
  function Rows({ section, done, details = false }) {
25
28
  const { entries, empty, lead } = section;
26
- return (_jsxs(_Fragment, { children: [lead ? (_jsx(Line, { quiet: true, onPress: () => {
29
+ return (_jsxs(_Fragment, { children: [lead ? (_jsx(Line, { icon: _jsx(FilePlus2, { size: 15, "aria-hidden": true }), onPress: () => {
27
30
  done?.();
28
31
  lead.run();
29
32
  }, children: lead.title })) : null, entries.map((e) => (_jsx(Line, { icon: e.icon, detail: details ? e.detail : undefined, onPress: e.open
@@ -43,7 +46,7 @@ export function Summary({ sections, onOpen, label = 'Summary' }) {
43
46
  setOpen(false);
44
47
  onOpen?.();
45
48
  };
46
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { children: _jsx(XStack, { render: "button", "data-slot": "summary-toggle", "aria-label": label, "aria-haspopup": "dialog", "aria-expanded": open, ...{ title: label }, width: 32, height: 32, rounded: "$3", items: "center", justify: "center", cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(ListTodo, { size: 16, "aria-hidden": true }) }) }), _jsx(PopoverContent, { solid: true, align: "end", p: 6, width: 300, children: _jsx(YStack, { "data-slot": "summary", role: "dialog", "aria-label": label, gap: 1, self: "stretch", width: "100%", maxH: "min(70dvh, 520px)", overflowY: "auto", overflowX: "hidden", children: sections.map((s, i) => (_jsxs(YStack, { gap: 1, children: [i ? _jsx(XStack, { height: 1, bg: "$borderColor", my: "$1.5", mx: "$1" }) : null, _jsx(Head, { action: s.action, children: s.title }), _jsx(Rows, { section: s, done: done })] }, s.id))) }) })] }));
49
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { children: _jsx(XStack, { render: "button", "data-slot": "summary-toggle", "aria-label": label, "aria-haspopup": "dialog", "aria-expanded": open, ...{ title: label }, width: 32, height: 32, rounded: "$3", items: "center", justify: "center", cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(ListTodo, { size: 16, "aria-hidden": true }) }) }), _jsx(PopoverContent, { solid: true, align: "end", sideOffset: 8, p: 4, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(YStack, { "data-slot": "summary", role: "dialog", "aria-label": label, self: "stretch", width: "100%", maxH: "min(70dvh, 520px)", overflowY: "auto", overflowX: "hidden", children: sections.map((s, i) => (_jsxs(YStack, { children: [i ? _jsx(XStack, { height: 1, bg: "$borderColor", my: 4, mx: 4 }) : null, _jsx(Head, { action: s.action, children: s.title }), _jsx(Rows, { section: s, done: done })] }, s.id))) }) })] }));
47
50
  }
48
51
  /** Each section as a kind of tab for `Panel`: the same rows, with their second lines. */
49
52
  export function kinds(sections, icons = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.70",
3
+ "version": "0.2.73",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",