@hanzo/build 0.2.52 → 0.2.54
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/README.md +103 -49
- package/lib/api/billing.d.ts +39 -0
- package/lib/api/billing.js +41 -0
- package/lib/api/coding.d.ts +4 -1
- package/lib/api/coding.js +2 -0
- package/lib/api/environment.d.ts +12 -0
- package/lib/api/environment.js +39 -1
- package/lib/api/github.d.ts +24 -4
- package/lib/api/github.js +60 -16
- package/lib/api/limits.d.ts +69 -0
- package/lib/api/limits.js +86 -0
- package/lib/api/links.d.ts +42 -0
- package/lib/api/links.js +65 -0
- package/lib/api/pref.d.ts +5 -3
- package/lib/api/pref.js +0 -3
- package/lib/api/turn.d.ts +10 -1
- package/lib/api/turn.js +16 -4
- package/lib/api/work.d.ts +75 -38
- package/lib/api/work.js +132 -44
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +12 -52
- package/lib/choice.d.ts +21 -0
- package/lib/choice.js +40 -1
- package/lib/customize/agents.d.ts +1 -1
- package/lib/customize/agents.js +21 -19
- package/lib/customize/connectors.d.ts +1 -1
- package/lib/customize/connectors.js +80 -31
- package/lib/customize/index.d.ts +2 -0
- package/lib/customize/index.js +23 -19
- package/lib/customize/load.d.ts +9 -0
- package/lib/customize/load.js +22 -0
- package/lib/customize/plugins.d.ts +1 -1
- package/lib/customize/plugins.js +21 -19
- package/lib/customize/say.d.ts +1 -0
- package/lib/customize/say.js +38 -0
- package/lib/customize/skills.d.ts +1 -1
- package/lib/customize/skills.js +23 -17
- package/lib/customize/ui.d.ts +28 -11
- package/lib/customize/ui.js +29 -21
- package/lib/data.d.ts +13 -0
- package/lib/data.js +70 -0
- package/lib/desk.d.ts +6 -1
- package/lib/desk.js +43 -19
- package/lib/environment.d.ts +11 -3
- package/lib/environment.js +53 -13
- package/lib/foot.js +10 -6
- package/lib/forge.js +139 -71
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +70 -38
- package/lib/merge.d.ts +223 -0
- package/lib/merge.js +380 -0
- package/lib/meter.d.ts +31 -0
- package/lib/meter.js +45 -0
- package/lib/mind.d.ts +60 -0
- package/lib/mind.js +187 -0
- package/lib/nav.d.ts +11 -0
- package/lib/nav.js +75 -0
- package/lib/panel.d.ts +46 -0
- package/lib/panel.js +116 -0
- package/lib/pick.d.ts +8 -0
- package/lib/pick.js +18 -1
- package/lib/plan.d.ts +78 -0
- package/lib/plan.js +133 -0
- package/lib/project.js +16 -6
- package/lib/prompt.d.ts +75 -0
- package/lib/prompt.js +125 -0
- package/lib/prose.js +14 -10
- package/lib/reply.d.ts +36 -0
- package/lib/reply.js +124 -0
- package/lib/route.d.ts +16 -0
- package/lib/route.js +40 -0
- package/lib/run.js +87 -52
- package/lib/section.d.ts +1 -1
- package/lib/section.js +7 -11
- package/lib/settings/billing.js +16 -9
- package/lib/settings/code.js +15 -12
- package/lib/settings/credits.d.ts +9 -0
- package/lib/settings/credits.js +110 -0
- package/lib/settings/plan.d.ts +17 -0
- package/lib/settings/plan.js +108 -0
- package/lib/settings/usage.js +11 -119
- package/lib/shelf.js +3 -3
- package/lib/standing.d.ts +44 -0
- package/lib/standing.js +64 -0
- package/lib/sync.js +22 -8
- package/lib/tabs.d.ts +53 -0
- package/lib/tabs.js +197 -0
- package/lib/tools.d.ts +2 -9
- package/lib/tools.js +20 -25
- package/lib/transcript.d.ts +10 -1
- package/lib/transcript.js +25 -10
- package/lib/voice.d.ts +2 -13
- package/lib/voice.js +7 -73
- package/package.json +20 -3
|
@@ -2,13 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Connectors: MCP servers whose tools an agent calls.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Browse is the shelf (our copy of the public MCP registry), then the fleet's
|
|
6
|
+
* own servers, which every run can call and nothing adds; that list is read
|
|
7
|
+
* only when asked for, since listing it asks every subsystem at once. A listing
|
|
8
|
+
* is added here only when it serves streamable HTTP; one that ships only a
|
|
9
|
+
* package needs somewhere to run it first, so it has no Add. Yours is the
|
|
10
|
+
* servers the org added — by URL, or off the shelf — each with one switch for
|
|
11
|
+
* all its tools on its card and one per tool inside; a tool is called only
|
|
12
|
+
* while it is on. Adding a server switches its tools on. A server's secret is
|
|
13
|
+
* sealed in KMS and never shown again.
|
|
12
14
|
*/
|
|
13
15
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
16
|
import { ChevronDown, ExternalLink, KeyRound, Server as ServerIcon } from '@hanzogui/lucide-icons-2';
|
|
@@ -17,24 +19,49 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
17
19
|
import { add, one, owns, ready, remove, servers, shelf, titleOf } from '../api/connectors.js';
|
|
18
20
|
import { natives } from '../api/mcp.js';
|
|
19
21
|
import { toggle, tools } from '../api/tools.js';
|
|
20
|
-
import { useRead } from '../data.js';
|
|
21
22
|
import { useHost, useTarget } from '../host.js';
|
|
22
23
|
import { Out } from '../out.js';
|
|
23
|
-
import {
|
|
24
|
+
import { route } from '../route.js';
|
|
25
|
+
import { useLoad } from './load.js';
|
|
26
|
+
import { say } from './say.js';
|
|
27
|
+
import { Add, Confirm, Empty, Failed, Featured, Field, Grid, left, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
|
|
24
28
|
const PAGE = 48;
|
|
25
29
|
/** The part of a URL a person reads: its host and path. */
|
|
26
30
|
const where = (url) => url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
27
|
-
export function Connectors({ view, q, adding, onAdding, onView }) {
|
|
31
|
+
export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
|
|
28
32
|
const host = useHost();
|
|
29
33
|
const t = useTarget();
|
|
30
34
|
const signed = Boolean(host.person);
|
|
31
35
|
// What every run in the org connects to is an org admin's to change; a member reads it.
|
|
32
36
|
const may = signed && host.admin;
|
|
33
|
-
const mine =
|
|
37
|
+
const mine = useLoad(signed ? () => servers(t) : null, [], [t, signed]);
|
|
38
|
+
// Every server's tools, for the switch on each card; read once for all of them.
|
|
39
|
+
const plane = useLoad(signed && view === 'yours' && mine.value.length ? () => tools(t, { source: 'mcp' }) : null, [], [t, signed, view, mine.value]);
|
|
34
40
|
const [opened, setOpened] = useState(null);
|
|
35
41
|
const [listing, setListing] = useState(null);
|
|
36
42
|
const [connecting, setConnecting] = useState(null);
|
|
43
|
+
const [busy, setBusy] = useState('');
|
|
37
44
|
const [note, setNote] = useState('');
|
|
45
|
+
useCount(mine.value.length, onCount);
|
|
46
|
+
/** Switch every tool a server brings on or off at once, then read them again. */
|
|
47
|
+
const flip = async (s, next) => {
|
|
48
|
+
const its = plane.value.filter((x) => owns(s, x.name)).map((x) => x.name);
|
|
49
|
+
if (!its.length)
|
|
50
|
+
return;
|
|
51
|
+
setBusy(s.id);
|
|
52
|
+
setNote('');
|
|
53
|
+
try {
|
|
54
|
+
await toggle(t, next ? its : [], next ? [] : its);
|
|
55
|
+
plane.reload();
|
|
56
|
+
setNote(next ? `${s.name} is on` : `${s.name} is off`);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
setNote(say(e));
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
setBusy('');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
38
65
|
const connect = adding || connecting ? (_jsx(Connect, { listing: connecting, onClose: () => {
|
|
39
66
|
setConnecting(null);
|
|
40
67
|
onAdding(false);
|
|
@@ -56,7 +83,21 @@ export function Connectors({ view, q, adding, onAdding, onView }) {
|
|
|
56
83
|
return _jsx(Visitor, { children: "Sign in to see your connectors." });
|
|
57
84
|
}
|
|
58
85
|
const shown = mine.value.filter((s) => matches(q, s.name, s.url, s.listing));
|
|
59
|
-
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(
|
|
86
|
+
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 connectors\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No connectors yet", detail: may ? 'Connect an app from Browse, or add any MCP server by its URL.' : 'An org admin adds connectors. Browse what there is.', children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse connectors" }), may ? (_jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "Add by URL" })) : null] })) : !shown.length ? (_jsx(Soft, { children: `No connector of yours matches “${q.trim()}”.` })) : (_jsx(Grid, { label: "Your connectors", children: shown.map((s) => {
|
|
87
|
+
const its = plane.value.filter((x) => owns(s, x.name));
|
|
88
|
+
const lit = its.filter((x) => x.activated).length;
|
|
89
|
+
return (_jsx(Tile, { title: s.name, detail: where(s.url), meta: [
|
|
90
|
+
its.length ? `${lit} of ${its.length} tools on` : plane.loading ? '' : 'No tools listed yet',
|
|
91
|
+
s.listing ? 'From Browse' : 'Added by URL',
|
|
92
|
+
s.secret ? 'Secret in KMS' : '',
|
|
93
|
+
s.admitted ? '' : 'Not in runs until an admin adds it again',
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' · '), mark: _jsx(Mark, { name: s.name }), onOpen: () => setOpened(s), action: may && its.length ? (_jsx(Switch, { checked: lit > 0, disabled: busy === s.id, onCheckedChange: (v) => void flip(s, v), "aria-label": `${s.name} on` })) : its.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: lit ? 'On' : 'Off' })) : undefined }, s.id));
|
|
97
|
+
}) })), plane.error && mine.value.length ? _jsx(Line, { children: `Tools: ${say(plane.error)}` }) : null, opened ? (_jsx(Detail, { server: opened, may: may, onClose: () => {
|
|
98
|
+
setOpened(null);
|
|
99
|
+
plane.reload();
|
|
100
|
+
}, onDeleted: () => {
|
|
60
101
|
setNote(`${opened.name} is removed`);
|
|
61
102
|
setOpened(null);
|
|
62
103
|
mine.reload();
|
|
@@ -66,19 +107,20 @@ export function Connectors({ view, q, adding, onAdding, onView }) {
|
|
|
66
107
|
function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
67
108
|
const t = useTarget();
|
|
68
109
|
const text = useSettled(q.trim(), 300);
|
|
69
|
-
const first =
|
|
110
|
+
const first = useLoad(signed ? () => shelf(t, { text, limit: PAGE }) : null, null, [t, signed, text]);
|
|
70
111
|
const [more, setMore] = useState([]);
|
|
71
112
|
const [paging, setPaging] = useState(false);
|
|
72
113
|
const [note, setNote] = useState('');
|
|
73
114
|
useEffect(() => setMore([]), [first.value]);
|
|
74
115
|
if (!signed) {
|
|
75
|
-
return (_jsx(Part, { title: "
|
|
116
|
+
return (_jsx(Part, { title: "Connect an app", detail: "Give runs the tools you already use.", children: _jsx(Visitor, { children: "Sign in to browse connectors." }) }));
|
|
76
117
|
}
|
|
77
118
|
const page = first.value;
|
|
78
119
|
const list = [...(page?.listings ?? []), ...more];
|
|
79
120
|
const hero = !text ? list.find((l) => l.featured) : undefined;
|
|
80
121
|
const rest = hero ? list.filter((l) => l !== hero) : list;
|
|
81
|
-
|
|
122
|
+
// A listing that ships only as a package has no Add: its card says it needs a place to run.
|
|
123
|
+
const action = (l) => (may && ready(l) ? _jsx(Add, { name: titleOf(l), added: added.has(l.id), onPress: () => onAdd(l) }) : undefined);
|
|
82
124
|
const next = async () => {
|
|
83
125
|
setPaging(true);
|
|
84
126
|
setNote('');
|
|
@@ -87,16 +129,16 @@ function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
|
87
129
|
setMore((m) => [...m, ...got.listings]);
|
|
88
130
|
}
|
|
89
131
|
catch (e) {
|
|
90
|
-
setNote(e
|
|
132
|
+
setNote(say(e));
|
|
91
133
|
}
|
|
92
134
|
finally {
|
|
93
135
|
setPaging(false);
|
|
94
136
|
}
|
|
95
137
|
};
|
|
96
|
-
return (_jsx(Part, { title: "
|
|
138
|
+
return (_jsx(Part, { title: "Connect an app", detail: "Give runs the tools you already use. Adding one turns its tools on for your organization.", children: first.error && !page ? (_jsx(Failed, { error: first.error, onRetry: first.reload })) : !page ? (_jsx(Soft, { children: "Loading connectors\u2026" })) : !list.length ? (_jsx(Soft, { children: text ? `No connector matches “${text}”.` : 'There are no connectors to add yet.' })) : (_jsxs(YStack, { gap: "$3", children: [hero ? (_jsx(Featured, { title: titleOf(hero), detail: hero.description, meta: facts(hero), mark: _jsx(Mark, { name: titleOf(hero), logo: hero.logo, size: 48 }), onOpen: () => onOpen(hero), action: action(hero) })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: list.length < page.total ? `${list.length} of ${page.total} connectors` : `${page.total} connectors` }), _jsx(Grid, { label: "Servers on the shelf", children: rest.map((l) => (_jsx(Tile, { title: titleOf(l), detail: l.description, meta: facts(l), mark: _jsx(Mark, { name: titleOf(l), logo: l.logo }), onOpen: () => onOpen(l), action: action(l) }, l.id))) }), _jsx(Line, { children: note }), list.length < page.total ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", variant: "outline", disabled: paging, onPress: () => void next(), children: "Show more" }) })) : null] })) }));
|
|
97
139
|
}
|
|
98
140
|
/** The facts a card carries under its description. */
|
|
99
|
-
const facts = (l) => [l.vendor, l.official ? 'Official' : '', ready(l) ? 'Hosted' : 'Package only'].filter(Boolean).join(' · ');
|
|
141
|
+
const facts = (l) => [l.vendor, l.official ? 'Official' : '', ready(l) ? 'Hosted' : 'Package only: needs a place to run'].filter(Boolean).join(' · ');
|
|
100
142
|
/** `value` once it has held still for `ms`. */
|
|
101
143
|
function useSettled(value, ms) {
|
|
102
144
|
const [settled, setSettled] = useState(value);
|
|
@@ -106,25 +148,32 @@ function useSettled(value, ms) {
|
|
|
106
148
|
}, [value, ms]);
|
|
107
149
|
return settled;
|
|
108
150
|
}
|
|
109
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The fleet's own servers, from the MCP server's tools/list. Every run can call
|
|
153
|
+
* them. Listed when asked for — that list starts every subsystem it names — and
|
|
154
|
+
* at once on the old MCP address, which exists to show them.
|
|
155
|
+
*/
|
|
110
156
|
function Natives({ q }) {
|
|
157
|
+
const host = useHost();
|
|
111
158
|
const t = useTarget();
|
|
112
|
-
const
|
|
159
|
+
const r = route(host.path);
|
|
160
|
+
const [asked, setAsked] = useState(r.kind === 'screen' && r.screen === 'mcp');
|
|
161
|
+
const list = useLoad(asked ? () => natives(t) : null, [], [t, asked]);
|
|
113
162
|
const [open, setOpen] = useState(null);
|
|
114
163
|
const shown = list.value.filter((s) => matches(q, s.name, s.description, ...s.ops));
|
|
115
164
|
const ops = shown.reduce((n, s) => n + s.ops.length, 0);
|
|
116
|
-
return (_jsx(Part, { title: "Built in", detail: "
|
|
165
|
+
return (_jsx(Part, { title: "Built in", detail: "Hanzo\u2019s own servers. Always on: every run can call them, and nothing needs adding.", action: asked ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAsked(true), children: "Show built-in servers" })), children: !asked ? null : list.loading && !list.value.length ? (_jsx(Soft, { children: "Loading built-in servers\u2026" })) : list.error && !list.value.length ? (_jsx(Failed, { error: list.error, onRetry: list.reload })) : !shown.length ? (_jsx(Soft, { children: list.value.length ? `No built-in server matches “${q.trim()}”.` : 'No built-in servers are listed.' })) : (_jsxs(YStack, { gap: "$1", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " servers \u00B7 ", ops, " operations"] }), shown.map((s) => {
|
|
117
166
|
const on = open === s.name;
|
|
118
|
-
return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, style: left, onPress: () => setOpen(on ? null : s.name), children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: s.name }), s.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: on ? undefined : 2, children: s.description })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: s.ops.length }), _jsx(ChevronDown, { size: 14, style: { transform: on ? 'rotate(180deg)' : undefined } })] }), on ? (_jsx(YStack, { px: "$3", py: "$2", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: s.ops.length ? (s.ops.map((op) => (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: op }, op)))) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "This server
|
|
167
|
+
return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, style: left, onPress: () => setOpen(on ? null : s.name), children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: s.name }), s.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: on ? undefined : 2, children: s.description })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: s.ops.length }), _jsx(ChevronDown, { size: 14, style: { transform: on ? 'rotate(180deg)' : undefined } })] }), on ? (_jsx(YStack, { px: "$3", py: "$2", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: s.ops.length ? (s.ops.map((op) => (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: op }, op)))) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "This server lists no operations." })) })) : null] }, s.name));
|
|
119
168
|
})] })) }));
|
|
120
169
|
}
|
|
121
170
|
/** One listing in full: what it is, where it is served, and adding it. */
|
|
122
171
|
function About({ listing, added, signed, onAdd, onClose }) {
|
|
123
172
|
const t = useTarget();
|
|
124
|
-
const full =
|
|
173
|
+
const full = useLoad(() => one(t, listing.id), listing, [t, listing.id]);
|
|
125
174
|
const l = full.value;
|
|
126
175
|
const links = [l.site, l.repo].filter((u) => u.startsWith('https://'));
|
|
127
|
-
return (_jsxs(Sheet, { title: titleOf(l), open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(Mark, { name: titleOf(l), logo: l.logo, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: mono, children: l.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [l.vendor, l.version ? `v${l.version}` : '', l.official ? 'Official' : ''].filter(Boolean).join(' · ') })] })] }), l.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: l.description })) : null, l.remotes.length ? (_jsx(Field, { label: "Served at", children: l.remotes.map((r) => (_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 1, children: [r.url, " \u00B7 ", r.transport] }, r.url))) })) : null, l.packages.length ? (_jsx(Field, { label: "Packages", hint: ready(l) ? undefined : 'It ships only as a package, so it needs
|
|
176
|
+
return (_jsxs(Sheet, { title: titleOf(l), open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(Mark, { name: titleOf(l), logo: l.logo, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: mono, children: l.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [l.vendor, l.version ? `v${l.version}` : '', l.official ? 'Official' : ''].filter(Boolean).join(' · ') })] })] }), l.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: l.description })) : null, l.remotes.length ? (_jsx(Field, { label: "Served at", children: l.remotes.map((r) => (_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 1, children: [r.url, " \u00B7 ", r.transport] }, r.url))) })) : null, l.packages.length ? (_jsx(Field, { label: "Packages", hint: ready(l) ? undefined : 'It ships only as a package, so it needs a place to run before it can be added here.', children: l.packages.map((p) => (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 1, children: [p.registry, p.identifier, p.version, p.runtime].filter(Boolean).join(' · ') }, `${p.registry}:${p.identifier}`))) })) : null, links.length ? (_jsx(XStack, { gap: "$4", flexWrap: "wrap", children: links.map((u) => (_jsxs(Out, { href: u, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: where(u) }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }, u))) })) : null, full.error ? _jsx(Line, { children: `Showing what Browse listed. ${say(full.error)}` }) : null, signed && ready(l) ? (_jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", disabled: added, onPress: onAdd, children: added ? 'Added' : 'Add to your connectors' }) })) : null] }));
|
|
128
177
|
}
|
|
129
178
|
/**
|
|
130
179
|
* Add a server: a listing off the shelf, or any MCP URL. The secret, when it
|
|
@@ -152,23 +201,23 @@ function Connect({ listing, onClose, onAdded }) {
|
|
|
152
201
|
said = its.length ? `${s.name} is added, and its ${its.length} tools are on.` : `${s.name} is added. It listed no tools yet.`;
|
|
153
202
|
}
|
|
154
203
|
catch (e) {
|
|
155
|
-
said = `${s.name} is added, but its tools could not be switched on
|
|
204
|
+
said = `${s.name} is added, but its tools could not be switched on. ${say(e)}`;
|
|
156
205
|
}
|
|
157
206
|
onAdded(said);
|
|
158
207
|
}
|
|
159
208
|
catch (e) {
|
|
160
|
-
setNote(e
|
|
209
|
+
setNote(say(e));
|
|
161
210
|
}
|
|
162
211
|
finally {
|
|
163
212
|
setWorking(false);
|
|
164
213
|
}
|
|
165
214
|
};
|
|
166
|
-
return (_jsxs(Sheet, { title: listing ? `Add ${titleOf(listing)}` : 'Add
|
|
215
|
+
return (_jsxs(Sheet, { title: listing ? `Add ${titleOf(listing)}` : 'Add by URL', open: true, onOpenChange: (o) => !o && onClose(), width: 500, children: [listing ? (_jsx(SizableText, { size: "$2", color: "$soft", children: listing.description || listing.name })) : (_jsx(SizableText, { size: "$2", color: "$soft", children: "Any MCP server that speaks HTTP. Its tools are on once it is added." })), _jsx(Field, { label: "Name", hint: listing ? 'Leave it empty to use the listing’s own.' : undefined, children: _jsx(Input, { value: name, onChangeText: setName, placeholder: listing ? titleOf(listing) : 'Docs', "aria-label": "Name" }) }), listing ? null : (_jsx(Field, { label: "URL", hint: "A public http(s) address. Private and loopback addresses are refused.", children: _jsx(Input, { value: url, onChangeText: setUrl, placeholder: "https://mcp.example.com/mcp", "aria-label": "URL", autoCapitalize: "none" }) })), _jsx(Field, { label: "Secret", hint: "Optional. Kept sealed in KMS, sent on every call, and never shown again.", children: _jsx(Input, { value: secret, onChangeText: setSecret, placeholder: "Bearer \u2026", "aria-label": "Secret", secureTextEntry: true }) }), secret ? (_jsx(Field, { label: "Header", children: _jsx(Input, { value: header, onChangeText: setHeader, "aria-label": "Header", autoCapitalize: "none" }) })) : null, _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Add" })] })] }));
|
|
167
216
|
}
|
|
168
217
|
/** One of the org's servers: where it is, its secret, its tools and their switches, and removing it. */
|
|
169
218
|
function Detail({ server, may, onClose, onDeleted }) {
|
|
170
219
|
const t = useTarget();
|
|
171
|
-
const list =
|
|
220
|
+
const list = useLoad(() => tools(t, { source: 'mcp' }), [], [t, server.id]);
|
|
172
221
|
const its = useMemo(() => list.value.filter((x) => owns(server, x.name)), [list.value, server]);
|
|
173
222
|
const [busy, setBusy] = useState(false);
|
|
174
223
|
const [note, setNote] = useState('');
|
|
@@ -182,16 +231,16 @@ function Detail({ server, may, onClose, onDeleted }) {
|
|
|
182
231
|
list.reload();
|
|
183
232
|
}
|
|
184
233
|
catch (e) {
|
|
185
|
-
setNote(e
|
|
234
|
+
setNote(say(e));
|
|
186
235
|
}
|
|
187
236
|
finally {
|
|
188
237
|
setBusy(false);
|
|
189
238
|
}
|
|
190
239
|
};
|
|
191
|
-
return (_jsxs(Sheet, { title: server.name, open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 2, children: server.url }), _jsxs(XStack, { items: "center", gap: "$1.5", children: [server.secret ? _jsx(KeyRound, { size: 12, opacity: 0.7 }) : _jsx(ServerIcon, { size: 12, opacity: 0.7 }), _jsx(SizableText, { size: "$1", color: "$soft", children: server.secret ? `A secret is sealed in KMS and sent in ${server.header || 'its header'}.` : 'No secret: it is called as it is.' })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", flex: 1, children: "Tools" }), its.length && may ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => void (off.length ? flip(off, []) : flip([], its.map((x) => x.name))), children: off.length ? 'Turn all on' : 'Turn all off' })) : null] }), list.loading && !list.value.length ? (_jsx(Soft, { children: "
|
|
240
|
+
return (_jsxs(Sheet, { title: server.name, open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 2, children: server.url }), _jsxs(XStack, { items: "center", gap: "$1.5", children: [server.secret ? _jsx(KeyRound, { size: 12, opacity: 0.7 }) : _jsx(ServerIcon, { size: 12, opacity: 0.7 }), _jsx(SizableText, { size: "$1", color: "$soft", children: server.secret ? `A secret is sealed in KMS and sent in ${server.header || 'its header'}.` : 'No secret: it is called as it is.' })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", flex: 1, children: "Tools" }), its.length && may ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => void (off.length ? flip(off, []) : flip([], its.map((x) => x.name))), children: off.length ? 'Turn all on' : 'Turn all off' })) : null] }), list.loading && !list.value.length ? (_jsx(Soft, { children: "Loading its tools\u2026" })) : list.error && !list.value.length ? (_jsx(Failed, { error: list.error, onRetry: list.reload })) : !its.length ? (_jsx(Soft, { children: "No tools listed yet. A server that does not answer shows its tools once it does." })) : (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: its.map((x, i) => {
|
|
192
241
|
const short = x.name.slice(server.id.length + 1);
|
|
193
242
|
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: short }), x.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })) : null] }), may ? (_jsx(Switch, { checked: x.activated, disabled: busy, onCheckedChange: (v) => void flip(v ? [x.name] : [], v ? [] : [x.name]), "aria-label": `${short} on` })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.activated ? 'On' : 'Off' }))] }, x.name));
|
|
194
|
-
}) })), _jsx(Line, { children: note || (may ? '' : '
|
|
243
|
+
}) })), _jsx(Line, { children: note || (may ? '' : 'Only an org admin can switch or remove connectors.') }), may ? (_jsx(XStack, { justify: "flex-start", children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Remove connector" }) })) : null, _jsx(Confirm, { what: server.name, says: "Its tools leave your organization and every agent, and its sealed secret is destroyed.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
195
244
|
await remove(t, server.id);
|
|
196
245
|
onDeleted();
|
|
197
246
|
} })] }));
|
package/lib/customize/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Tab } from '../route.ts';
|
|
2
2
|
import { type View } from './ui.tsx';
|
|
3
|
+
/** What every run in the org loads: an org admin's to add to, a member's to read. */
|
|
4
|
+
export declare const ORG_KIT: Tab[];
|
|
3
5
|
export declare function Customize({ tab, view: first }: {
|
|
4
6
|
tab: Tab;
|
|
5
7
|
view?: View;
|
package/lib/customize/index.js
CHANGED
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Customize: what the agent brings to a run — the skills it knows, the
|
|
4
4
|
* connectors it calls, the plugins it runs, and agents of the org's own. Each
|
|
5
|
-
* tab is its own address
|
|
6
|
-
* what
|
|
5
|
+
* tab is its own address. In each, Browse comes first and is where the page
|
|
6
|
+
* opens: what there is to add, then what Hanzo builds in. Yours is what the org
|
|
7
|
+
* already has, counted on its button. One search and one Add serve both.
|
|
7
8
|
*
|
|
8
9
|
* Skills and connectors ride into every run in the org, so adding, switching
|
|
9
10
|
* and removing them is an org admin's and a member reads them; plugins and
|
|
@@ -12,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
13
14
|
import { BookOpen, Bot, Plug, Plus, Puzzle } from '@hanzogui/lucide-icons-2';
|
|
14
15
|
import { Button, Input } from '@hanzo/ui';
|
|
15
|
-
import { useState } from 'react';
|
|
16
|
+
import { useCallback, useState } from 'react';
|
|
16
17
|
import { useHost } from '../host.js';
|
|
17
18
|
import { path, TABS } from '../route.js';
|
|
18
19
|
import { Agents } from './agents.js';
|
|
@@ -21,33 +22,36 @@ import { Plugins } from './plugins.js';
|
|
|
21
22
|
import { Skills } from './skills.js';
|
|
22
23
|
import { Choice } from './ui.js';
|
|
23
24
|
/** What every run in the org loads: an org admin's to add to, a member's to read. */
|
|
24
|
-
const ORG_KIT = ['skills', 'connectors'];
|
|
25
|
+
export const ORG_KIT = ['skills', 'connectors'];
|
|
25
26
|
const LABEL = {
|
|
26
|
-
skills: { title: 'Skills', add: 'New skill', find: 'Search skills', icon: _jsx(BookOpen, { size: 15 }) },
|
|
27
|
-
connectors: { title: 'Connectors', add: 'Add
|
|
28
|
-
plugins: { title: 'Plugins', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
|
|
29
|
-
agents: { title: 'Agents', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
|
|
27
|
+
skills: { title: 'Skills', says: 'Know-how an agent reads before it works.', add: 'New skill', find: 'Search skills', icon: _jsx(BookOpen, { size: 15 }) },
|
|
28
|
+
connectors: { title: 'Connectors', says: 'Apps and tools an agent can call.', add: 'Add by URL', find: 'Search connectors', icon: _jsx(Plug, { size: 15 }) },
|
|
29
|
+
plugins: { title: 'Plugins', says: 'Connectors you build in TypeScript.', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
|
|
30
|
+
agents: { title: 'Agents', says: 'Agents with their own model, instructions and budget.', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
|
|
30
31
|
};
|
|
31
|
-
const VIEWS = [
|
|
32
|
-
{ id: 'yours', label: 'Yours' },
|
|
33
|
-
{ id: 'discover', label: 'Discover' },
|
|
34
|
-
];
|
|
35
32
|
export function Customize({ tab, view: first }) {
|
|
36
33
|
const host = useHost();
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
const [picked, setView] = useState(first ?? null);
|
|
40
|
-
const view = picked ?? (host.person ? 'yours' : 'discover');
|
|
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');
|
|
41
36
|
const [q, setQ] = useState('');
|
|
42
37
|
const [adding, setAdding] = useState(false);
|
|
38
|
+
const [count, setCount] = useState(0);
|
|
39
|
+
const onCount = useCallback((n) => setCount(n), []);
|
|
43
40
|
const pick = (next) => {
|
|
44
41
|
setQ('');
|
|
45
42
|
setAdding(false);
|
|
43
|
+
setView('discover');
|
|
44
|
+
setCount(0);
|
|
46
45
|
host.go(path({ kind: 'customize', tab: next }));
|
|
47
46
|
};
|
|
48
|
-
const pane = { view, q, adding, onAdding: setAdding, onView: setView };
|
|
49
|
-
|
|
47
|
+
const pane = { view, q, adding, onAdding: setAdding, onView: setView, onCount };
|
|
48
|
+
const kit = ORG_KIT.includes(tab);
|
|
49
|
+
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) => {
|
|
50
51
|
const on = id === tab;
|
|
51
52
|
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));
|
|
52
|
-
}) }),
|
|
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
|
+
{ id: 'discover', label: 'Browse' },
|
|
55
|
+
{ id: 'yours', label: 'Yours', count },
|
|
56
|
+
], 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 }))] }) }));
|
|
53
57
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customize's reads: data.ts `useRead`, given up on after `WAIT`. A read that
|
|
3
|
+
* hangs says so and offers a retry instead of loading forever; the request
|
|
4
|
+
* itself is left to finish or fail on its own.
|
|
5
|
+
*/
|
|
6
|
+
import { type Read } from '../data.ts';
|
|
7
|
+
/** `p`, or a `Slow` refusal once `ms` pass first. */
|
|
8
|
+
export declare function within<T>(p: Promise<T>, ms?: number): Promise<T>;
|
|
9
|
+
export declare function useLoad<T>(load: (() => Promise<T>) | null, initial: T, key: unknown[]): Read<T>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customize's reads: data.ts `useRead`, given up on after `WAIT`. A read that
|
|
3
|
+
* hangs says so and offers a retry instead of loading forever; the request
|
|
4
|
+
* itself is left to finish or fail on its own.
|
|
5
|
+
*/
|
|
6
|
+
import { Slow, useRead, WAIT } from '../data.js';
|
|
7
|
+
/** `p`, or a `Slow` refusal once `ms` pass first. */
|
|
8
|
+
export function within(p, ms = WAIT) {
|
|
9
|
+
return new Promise((ok, no) => {
|
|
10
|
+
const id = setTimeout(() => no(new Slow()), ms);
|
|
11
|
+
p.then((v) => {
|
|
12
|
+
clearTimeout(id);
|
|
13
|
+
ok(v);
|
|
14
|
+
}, (e) => {
|
|
15
|
+
clearTimeout(id);
|
|
16
|
+
no(e);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function useLoad(load, initial, key) {
|
|
21
|
+
return useRead(load ? () => within(load()) : null, initial, key);
|
|
22
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type Pane } from './ui.tsx';
|
|
2
|
-
export declare function Plugins({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;
|
|
2
|
+
export declare function Plugins({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;
|
package/lib/customize/plugins.js
CHANGED
|
@@ -2,13 +2,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
/**
|
|
3
3
|
* Plugins: TypeScript connectors the org builds for the runtime to run.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Browse offers building one, then lists what this deployment mounts, which is
|
|
6
|
+
* built in and read-only. Yours is what the org built, each with the source that
|
|
7
|
+
* runs. Building is the gate — the platform bundles and compiles the source and
|
|
8
|
+
* keeps it only if both succeed, and a failure says why in the bundler's words.
|
|
9
|
+
* Describe an API instead of writing the TypeScript and a model writes it; the
|
|
10
|
+
* result opens to be read. A plugin carries no credential: it names the
|
|
11
|
+
* connector whose credential it reads when it runs.
|
|
12
12
|
*/
|
|
13
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
14
|
import { Puzzle } from '@hanzogui/lucide-icons-2';
|
|
@@ -16,18 +16,20 @@ import { Button, Input, Textarea } from '@hanzo/ui';
|
|
|
16
16
|
import { Code } from '@hanzo/ui/chat';
|
|
17
17
|
import { useState } from 'react';
|
|
18
18
|
import { authored, build, mounted, remove } from '../api/plugins.js';
|
|
19
|
-
import { useRead } from '../data.js';
|
|
20
19
|
import { useHost, useTarget } from '../host.js';
|
|
21
|
-
import {
|
|
20
|
+
import { useLoad } from './load.js';
|
|
21
|
+
import { say } from './say.js';
|
|
22
|
+
import { Choice, Confirm, day, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
|
|
22
23
|
const kb = (n) => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
|
|
23
|
-
export function Plugins({ view, q, adding, onAdding, onView }) {
|
|
24
|
+
export function Plugins({ view, q, adding, onAdding, onView, onCount }) {
|
|
24
25
|
const host = useHost();
|
|
25
26
|
const t = useTarget();
|
|
26
27
|
const signed = Boolean(host.person);
|
|
27
|
-
const mine =
|
|
28
|
-
const mounts =
|
|
28
|
+
const mine = useLoad(signed ? () => authored(t) : null, [], [t, signed]);
|
|
29
|
+
const mounts = useLoad(signed && view === 'discover' ? () => mounted(t) : null, [], [t, signed, view]);
|
|
29
30
|
const [opened, setOpened] = useState(null);
|
|
30
31
|
const [note, setNote] = useState('');
|
|
32
|
+
useCount(mine.value.length, onCount);
|
|
31
33
|
const sheets = (_jsxs(_Fragment, { children: [adding ? (_jsx(Build, { onClose: () => onAdding(false), onBuilt: (p, said) => {
|
|
32
34
|
onAdding(false);
|
|
33
35
|
onView('yours');
|
|
@@ -40,14 +42,14 @@ export function Plugins({ view, q, adding, onAdding, onView }) {
|
|
|
40
42
|
mine.reload();
|
|
41
43
|
} })) : null] }));
|
|
42
44
|
if (!signed) {
|
|
43
|
-
return _jsx(Visitor, { children: view === 'discover' ? 'Sign in to
|
|
45
|
+
return _jsx(Visitor, { children: view === 'discover' ? 'Sign in to browse plugins.' : 'Sign in to see your plugins.' });
|
|
44
46
|
}
|
|
45
47
|
if (view === 'discover') {
|
|
46
48
|
const shown = mounts.value.filter((m) => matches(q, m.name, ...m.prefixes));
|
|
47
|
-
return (_jsxs(YStack, { gap: "$
|
|
49
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Line, { children: note }), _jsx(Part, { title: "Build your own", detail: "Write a connector in TypeScript, or describe an API and Hanzo writes it.", children: _jsx(XStack, { children: _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "Build a plugin" }) }) }), _jsx(Part, { title: "Built in", detail: "Part of Hanzo. Always on, nothing to add.", children: mounts.error && !mounts.value.length ? (_jsx(Failed, { error: mounts.error, onRetry: mounts.reload })) : mounts.loading && !mounts.value.length ? (_jsx(Soft, { children: "Loading built-in plugins\u2026" })) : !shown.length ? (_jsx(Soft, { children: mounts.value.length ? `Nothing built in matches “${q.trim()}”.` : 'Nothing built in is listed.' })) : (_jsxs(_Fragment, { children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " built in"] }), _jsx(Grid, { label: "Mounted subsystems", children: shown.map((m) => (_jsx(Tile, { title: m.name, detail: m.prefixes.join(' '), meta: m.enabled ? 'On' : 'Off', mark: _jsx(Mark, { name: m.name, icon: _jsx(Puzzle, { size: 15 }) }) }, m.name))) })] })) }), sheets] }));
|
|
48
50
|
}
|
|
49
51
|
const shown = mine.value.filter((p) => matches(q, p.name, p.provider));
|
|
50
|
-
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(
|
|
52
|
+
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 plugins\u2026" })) : !mine.value.length ? (_jsx(Empty, { title: "No plugins yet", detail: "Write a connector in TypeScript, or describe an API and Hanzo writes it.", children: _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "Build a plugin" }) })) : !shown.length ? (_jsx(Soft, { children: `No plugin of yours matches “${q.trim()}”.` })) : (_jsx(Grid, { label: "Your plugins", children: shown.map((p) => (_jsx(Tile, { title: p.name, detail: p.provider ? `Reads the ${p.provider} connector’s credential when it runs.` : 'Needs no credential.', meta: day(p.built) ? `Built ${day(p.built)}` : undefined, mark: _jsx(Mark, { name: p.name }), onOpen: () => setOpened(p) }, p.id))) })), sheets] }));
|
|
51
53
|
}
|
|
52
54
|
const WAYS = [
|
|
53
55
|
{ id: 'source', label: 'TypeScript' },
|
|
@@ -72,15 +74,15 @@ function Build({ onClose, onBuilt }) {
|
|
|
72
74
|
onBuilt(out.plugin, `${out.plugin.name} built: ${kb(out.bytes)}${out.generated ? ', written from your description — read it below' : ''}.`);
|
|
73
75
|
}
|
|
74
76
|
catch (e) {
|
|
75
|
-
setNote(e
|
|
77
|
+
setNote(say(e));
|
|
76
78
|
}
|
|
77
79
|
finally {
|
|
78
80
|
setWorking(false);
|
|
79
81
|
}
|
|
80
82
|
};
|
|
81
|
-
return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -.
|
|
82
|
-
? '
|
|
83
|
-
: 'An OpenAPI document, or
|
|
83
|
+
return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -.", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name", autoCapitalize: "none" }) }), _jsx(Field, { label: "Connector", hint: "Optional. The connector whose key it uses when it runs; a plugin never holds a key itself.", children: _jsx(Input, { value: provider, onChangeText: setProvider, "aria-label": "Connector", autoCapitalize: "none" }) }), _jsx(Choice, { label: "Build from", value: way, options: WAYS, onChange: setWay }), _jsx(Field, { label: way === 'source' ? 'Source' : 'The API', hint: way === 'source'
|
|
84
|
+
? 'Kept only if it builds. A failed build says why.'
|
|
85
|
+
: 'An OpenAPI document, or the endpoints in plain words. You read the TypeScript before it runs.', children: _jsx(Textarea, { value: way === 'source' ? source : spec, onChangeText: way === 'source' ? setSource : setSpec, "aria-label": way === 'source' ? 'Source' : 'The API', placeholder: way === 'source' ? 'export const acme = createProvider({ … })' : 'POST /v1/things creates a thing…', rows: 14, style: mono }) }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: "Build" })] })] }));
|
|
84
86
|
}
|
|
85
87
|
/** A built plugin's source, as the runtime will run it, and deleting it. */
|
|
86
88
|
function Source({ plugin, onClose, onDeleted }) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function say(e: unknown): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a read or a write that failed tells a person: plain words, never the
|
|
3
|
+
* platform's internals. A refusal is read by its status; only a 400, 409 or
|
|
4
|
+
* 422 carries the platform's own sentence, because those name what was wrong
|
|
5
|
+
* with what was sent (a name taken, a build that failed and why). A sentence
|
|
6
|
+
* this client wrote itself (a name that does not fit) is said as written.
|
|
7
|
+
*/
|
|
8
|
+
import { Refusal } from '../api/call.js';
|
|
9
|
+
import { Slow } from '../data.js';
|
|
10
|
+
/** The line call.ts writes when the platform gave no reason: a method, a path and a status. */
|
|
11
|
+
const BARE = /^(GET|POST|PUT|PATCH|DELETE) \S* answered \d+$/;
|
|
12
|
+
export function say(e) {
|
|
13
|
+
if (e instanceof Slow)
|
|
14
|
+
return 'Hanzo is taking too long to answer. Try again.';
|
|
15
|
+
if (e instanceof Refusal) {
|
|
16
|
+
if (e.status === 401)
|
|
17
|
+
return 'Your sign-in has expired. Sign in again.';
|
|
18
|
+
if (e.status === 403)
|
|
19
|
+
return 'Your account can’t do this here. Ask an org admin.';
|
|
20
|
+
if (e.status === 404)
|
|
21
|
+
return 'Not found. It may have been removed.';
|
|
22
|
+
if (e.status === 429)
|
|
23
|
+
return 'Too many requests. Wait a moment and try again.';
|
|
24
|
+
if (e.status >= 500)
|
|
25
|
+
return 'Hanzo could not answer just now. Try again.';
|
|
26
|
+
if ((e.status === 400 || e.status === 409 || e.status === 422) && e.message && !BARE.test(e.message))
|
|
27
|
+
return e.message;
|
|
28
|
+
return 'Hanzo could not do that. Try again.';
|
|
29
|
+
}
|
|
30
|
+
// fetch rejects with a TypeError when the network does; a body that is not JSON is a SyntaxError.
|
|
31
|
+
if (e instanceof TypeError)
|
|
32
|
+
return 'Could not reach Hanzo. Check your connection and try again.';
|
|
33
|
+
if (e instanceof SyntaxError)
|
|
34
|
+
return 'Hanzo answered with something unreadable. Try again.';
|
|
35
|
+
if (e instanceof Error && e.message)
|
|
36
|
+
return e.message;
|
|
37
|
+
return 'Something went wrong. Try again.';
|
|
38
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type Pane } from './ui.tsx';
|
|
2
|
-
export declare function Skills({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;
|
|
2
|
+
export declare function Skills({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;
|