@hanzo/build 0.2.80 → 0.2.81
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 +13 -0
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +1 -20
- package/lib/api/agents.js +16 -8
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +2 -1
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.js +78 -31
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +43 -10
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +6 -3
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +18 -15
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/customize/agents.js
CHANGED
|
@@ -15,8 +15,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
15
15
|
* so the choices are the tools that are on; "every tool" is whatever the
|
|
16
16
|
* fleet's MCP server serves when it runs. A budget is required: what it may
|
|
17
17
|
* spend each period, and in one run.
|
|
18
|
+
*
|
|
19
|
+
* Its words are the reader's language (@hanzo/i18n `t`); an agent's name,
|
|
20
|
+
* description, instructions, model and runs, and the categories the host and
|
|
21
|
+
* the platform give their templates, are drawn as given.
|
|
18
22
|
*/
|
|
19
23
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
24
|
+
import { phrase } from '@hanzo/i18n';
|
|
25
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
20
26
|
import { ArrowLeft, Upload } from '@hanzogui/lucide-icons-2';
|
|
21
27
|
import { Button, Input, Switch, Textarea } from '@hanzo/ui';
|
|
22
28
|
import { ModelPicker } from '@hanzo/ui/models';
|
|
@@ -31,7 +37,7 @@ import { say } from './say.js';
|
|
|
31
37
|
import { Add, Choice, Confirm, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
|
|
32
38
|
/** How many cards Browse draws at a time. */
|
|
33
39
|
const PAGE = 60;
|
|
34
|
-
const ANY = 'All';
|
|
40
|
+
const ANY = phrase('All');
|
|
35
41
|
/** Categories as Browse offers them: the default agents' and presets' first, as listed, then the rest by how many each holds. */
|
|
36
42
|
export function kinds(first, rest) {
|
|
37
43
|
const lead = [...new Set(first.map((c) => c.category))];
|
|
@@ -42,13 +48,40 @@ export function kinds(first, rest) {
|
|
|
42
48
|
return [ANY, ...lead, ...[...count.keys()].sort((a, b) => count.get(b) - count.get(a) || a.localeCompare(b))];
|
|
43
49
|
}
|
|
44
50
|
const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')}`;
|
|
51
|
+
/** A category as Browse names it: All, and the platform's presets and people, in the reader's words; the rest are the host's, as given. */
|
|
52
|
+
const category = (c, t) => (c === ANY || c === 'Preset' || c === 'Person' ? t(c) : c);
|
|
53
|
+
/**
|
|
54
|
+
* A budget's sentences by its period ('' where the agent names none): whole
|
|
55
|
+
* sentences, so a language says the period its own way.
|
|
56
|
+
*/
|
|
57
|
+
const SPENT = {
|
|
58
|
+
day: phrase('{spent} of {cap} this day'),
|
|
59
|
+
week: phrase('{spent} of {cap} this week'),
|
|
60
|
+
month: phrase('{spent} of {cap} this month'),
|
|
61
|
+
'': phrase('{spent} of {cap} this period'),
|
|
62
|
+
};
|
|
63
|
+
const BUDGET = {
|
|
64
|
+
day: phrase('{spent} of {cap} spent this day; at most {task} a run.'),
|
|
65
|
+
week: phrase('{spent} of {cap} spent this week; at most {task} a run.'),
|
|
66
|
+
month: phrase('{spent} of {cap} spent this month; at most {task} a run.'),
|
|
67
|
+
'': phrase('{spent} of {cap} spent this period; at most {task} a run.'),
|
|
68
|
+
};
|
|
69
|
+
const SO_FAR = {
|
|
70
|
+
day: phrase('Spent {spent} this day.'),
|
|
71
|
+
week: phrase('Spent {spent} this week.'),
|
|
72
|
+
month: phrase('Spent {spent} this month.'),
|
|
73
|
+
'': phrase('Spent {spent} this period.'),
|
|
74
|
+
};
|
|
75
|
+
const EACH = { day: phrase('Each day, USD'), week: phrase('Each week, USD'), month: phrase('Each month, USD') };
|
|
76
|
+
const PERIOD = { day: phrase('Day'), week: phrase('Week'), month: phrase('Month') };
|
|
45
77
|
export function Agents({ view, q, adding, onAdding, onView, onCount }) {
|
|
78
|
+
const { t } = useI18n();
|
|
46
79
|
const host = useHost();
|
|
47
|
-
const
|
|
80
|
+
const target = useTarget();
|
|
48
81
|
const signed = Boolean(host.person);
|
|
49
82
|
const browsing = view === 'discover';
|
|
50
|
-
const mine = useLoad(signed ? () => agents(
|
|
51
|
-
const offered = useLoad(browsing ? () => presets(
|
|
83
|
+
const mine = useLoad(signed ? () => agents(target) : null, [], [target, signed]);
|
|
84
|
+
const offered = useLoad(browsing ? () => presets(target) : null, [], [target, view]);
|
|
52
85
|
const figures = useLoad(browsing && host.personas ? host.personas : null, [], [view]);
|
|
53
86
|
useCount(mine.value.length, onCount);
|
|
54
87
|
const [note, setNote] = useState('');
|
|
@@ -62,10 +95,10 @@ export function Agents({ view, q, adding, onAdding, onView, onCount }) {
|
|
|
62
95
|
setUsing(c);
|
|
63
96
|
setNote('');
|
|
64
97
|
try {
|
|
65
|
-
opened(await use(
|
|
98
|
+
opened(await use(target, c, window.location.href));
|
|
66
99
|
}
|
|
67
100
|
catch (e) {
|
|
68
|
-
setNote(say(e));
|
|
101
|
+
setNote(say(e, t));
|
|
69
102
|
}
|
|
70
103
|
finally {
|
|
71
104
|
setUsing(null);
|
|
@@ -75,25 +108,33 @@ export function Agents({ view, q, adding, onAdding, onView, onCount }) {
|
|
|
75
108
|
onAdding(false);
|
|
76
109
|
onView('yours');
|
|
77
110
|
mine.reload();
|
|
78
|
-
setNote(
|
|
111
|
+
setNote(t('{name} is saved', { name: a.name }));
|
|
79
112
|
} })) : null;
|
|
80
113
|
if (browsing) {
|
|
81
114
|
const first = [...(host.cast ?? []), ...offered.value.map(fromPreset)];
|
|
82
115
|
const all = [...first, ...figures.value];
|
|
83
116
|
const shown = all.filter((c) => (kind === ANY || c.category === kind) && matches(q, c.title, c.name, c.description, c.category));
|
|
84
117
|
const failed = figures.error ?? offered.error;
|
|
85
|
-
return (_jsxs(YStack, { gap: "$3", children: [signed ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children:
|
|
118
|
+
return (_jsxs(YStack, { gap: "$3", children: [signed ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children: t('Start from scratch') }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setMaking('self'), children: t('Clone yourself') }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setMaking('person'), children: t('Create from a person') })] })) : null, _jsx(XStack, { overflowX: "auto", overflowY: "hidden", pb: "$1", children: _jsx(Choice, { label: t('Category'), value: kind, options: kinds(first, figures.value).map((id) => ({ id, label: category(id, t) })), onChange: setKind }) }), _jsx(Line, { children: note }), shown.length ? (_jsx(Grid, { label: t('Agents to start from'), children: shown.slice(0, more).map((c) => (_jsx(Tile, { title: c.title, detail: c.description === c.title || c.description === c.category ? undefined : c.description, meta: category(c.category, t), mark: _jsx(Mark, { name: c.title, size: 40, logo: c.avatar }), action: signed ? _jsx(Add, { name: c.title, use: true, added: false, busy: using !== null, onPress: () => void take(c) }) : undefined }, `${c.category}/${c.name}/${c.title}`))) })) : figures.loading || offered.loading ? null : (_jsx(Soft, { children: t('No agent matches “{q}”.', { q: q.trim() }) })), shown.length > more ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setMore(more + PAGE), children: t('More') }) })) : null, failed ? _jsx(Failed, { error: failed, onRetry: figures.error ? figures.reload : offered.reload }) : figures.loading || offered.loading ? _jsx(Soft, { children: t('Loading…') }) : null, editor, making ? _jsx(Person, { self: making === 'self', onClose: () => setMaking(null), onMade: opened }) : null] }));
|
|
86
119
|
}
|
|
87
120
|
if (!signed) {
|
|
88
|
-
return _jsx(Visitor, { children:
|
|
121
|
+
return _jsx(Visitor, { children: t('Sign in to see your agents.') });
|
|
89
122
|
}
|
|
90
123
|
const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
|
|
91
|
-
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:
|
|
124
|
+
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: t('Loading your agents…') })) : !mine.value.length ? (_jsxs(Empty, { title: t('No agents yet'), detail: "", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: t('Browse agents') }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: t('New agent') })] })) : !shown.length ? (_jsx(Soft, { children: t('No agent of yours matches “{q}”.', { q: q.trim() }) })) : (_jsx(Grid, { label: t('Your agents'), children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || t('No description.'), meta: [
|
|
125
|
+
a.model,
|
|
126
|
+
a.runs === 1 ? t('1 run') : t('{n} runs', { n: a.runs }),
|
|
127
|
+
reach(a, t),
|
|
128
|
+
a.cap ? t(SPENT[a.period], { spent: money(a.spent), cap: money(a.cap) }) : '',
|
|
129
|
+
]
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join(' · '), mark: _jsx(Face, { agent: a, cast: host.cast ?? [] }), onOpen: () => opened(a) }, a.id || a.name))) })), editor] }));
|
|
92
132
|
}
|
|
93
133
|
/** Clone yourself, or anyone named: a name, who they are, and writing of theirs, pasted or uploaded. */
|
|
94
134
|
function Person({ self, onClose, onMade }) {
|
|
135
|
+
const { t } = useI18n();
|
|
95
136
|
const host = useHost();
|
|
96
|
-
const
|
|
137
|
+
const target = useTarget();
|
|
97
138
|
const me = self ? host.person : null;
|
|
98
139
|
const [name, setName] = useState(me?.name ?? '');
|
|
99
140
|
const [about, setAbout] = useState('');
|
|
@@ -109,22 +150,22 @@ function Person({ self, onClose, onMade }) {
|
|
|
109
150
|
setWorking(true);
|
|
110
151
|
setNote('');
|
|
111
152
|
try {
|
|
112
|
-
onMade(await use(
|
|
153
|
+
onMade(await use(target, person(name, about, writing, me?.avatar), window.location.href));
|
|
113
154
|
}
|
|
114
155
|
catch (e) {
|
|
115
|
-
setNote(say(e));
|
|
156
|
+
setNote(say(e, t));
|
|
116
157
|
setWorking(false);
|
|
117
158
|
}
|
|
118
159
|
};
|
|
119
|
-
return (_jsxs(Sheet, { title: self ? 'Clone yourself' : 'Create from a person', open: true, onOpenChange: (o) => !o && onClose(), children: [_jsx(Field, { label:
|
|
160
|
+
return (_jsxs(Sheet, { title: self ? t('Clone yourself') : t('Create from a person'), open: true, onOpenChange: (o) => !o && onClose(), children: [_jsx(Field, { label: t('Name'), children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": t('Name') }) }), _jsx(Field, { label: t('Description'), children: _jsx(Textarea, { value: about, onChangeText: setAbout, "aria-label": t('Description'), rows: 3 }) }), _jsx(Field, { label: t('Writing'), children: _jsx(Textarea, { value: writing, onChangeText: setWriting, "aria-label": t('Writing'), rows: 6 }) }), _jsxs(XStack, { children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => file.current?.click(), children: [_jsx(Upload, { size: 14 }), " ", t('Upload')] }), _jsx("input", { ref: file, type: "file", multiple: true, accept: ".txt,.md,.markdown,.csv,.json,.html,text/*", hidden: true, "aria-label": t('Upload writing'), onChange: (e) => {
|
|
120
161
|
void read(e.currentTarget.files);
|
|
121
162
|
e.currentTarget.value = '';
|
|
122
|
-
} })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children:
|
|
163
|
+
} })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working || !name.trim(), onPress: () => void save(), children: t('Create') })] })] }));
|
|
123
164
|
}
|
|
124
|
-
function reach(a) {
|
|
165
|
+
function reach(a, t) {
|
|
125
166
|
if (a.tools.includes(ALL))
|
|
126
|
-
return 'every tool';
|
|
127
|
-
return a.tools.length === 1 ? '1 tool' :
|
|
167
|
+
return t('every tool');
|
|
168
|
+
return a.tools.length === 1 ? t('1 tool') : t('{n} tools', { n: a.tools.length });
|
|
128
169
|
}
|
|
129
170
|
/** An agent's face: its picture, the default agent's of its name, its emoji, or its initial. */
|
|
130
171
|
function Face({ agent: a, cast, size = 32 }) {
|
|
@@ -133,23 +174,25 @@ function Face({ agent: a, cast, size = 32 }) {
|
|
|
133
174
|
}
|
|
134
175
|
/** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
|
|
135
176
|
export function Profile({ name }) {
|
|
177
|
+
const { t } = useI18n();
|
|
136
178
|
const host = useHost();
|
|
137
|
-
const
|
|
138
|
-
const got = useLoad(host.person ? () => profile(
|
|
179
|
+
const target = useTarget();
|
|
180
|
+
const got = useLoad(host.person ? () => profile(target, name) : null, null, [target, name]);
|
|
139
181
|
const [editing, setEditing] = useState(false);
|
|
140
182
|
const back = () => host.go(path({ kind: 'customize', tab: 'agents' }));
|
|
141
183
|
const a = got.value?.agent;
|
|
142
|
-
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":
|
|
184
|
+
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": t('Back to agents'), children: [_jsx(ArrowLeft, { size: 14, color: "$soft", 'data-i18n-mirror': '' }), _jsx(SizableText, { size: "$2", color: "$soft", children: t('Agents') })] }), !host.person ? (_jsx(Visitor, { children: t('Sign in to see this agent.') })) : got.error && !a ? (_jsx(Failed, { error: got.error, onRetry: got.reload })) : !a ? (_jsx(Soft, { children: t('Loading the agent…') })) : (_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 || t('No role written yet.') })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditing(true), children: t('Edit') })] }), _jsx(Part, { title: t('Model'), detail: a.model || t('The deployment’s default') }), _jsx(Part, { title: t('Instructions'), detail: a.instructions ? '' : t('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: t('Tools'), detail: a.tools.includes(ALL) ? t('Every tool the fleet’s MCP server serves.') : a.tools.length ? a.tools.join(', ') : t('None: it calls no tool.') }), _jsx(Part, { title: t('Budget'), detail: a.cap ? t(BUDGET[a.period], { spent: money(a.spent), cap: money(a.cap), task: money(a.task) }) : t('No budget set.') }), _jsx(Part, { title: t('Runs'), detail: a.runs ? t('{runs} in all; the latest {n} below.', { runs: a.runs, n: Math.min(a.runs, got.value.runs.length) }) : t('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' ? t('ok') : t('failed') }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: r.input || t('(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: () => {
|
|
143
185
|
setEditing(false);
|
|
144
186
|
got.reload();
|
|
145
187
|
}, onDeleted: back })) : null] }));
|
|
146
188
|
}
|
|
147
189
|
/** Write a new agent, or change one; an existing agent is read whole first, for its instructions. */
|
|
148
190
|
function Editor({ agent, onClose, onSaved, onDeleted, }) {
|
|
149
|
-
const t =
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const
|
|
191
|
+
const { t } = useI18n();
|
|
192
|
+
const target = useTarget();
|
|
193
|
+
const full = useLoad(agent ? () => one(target, agent.id || agent.name) : null, null, [target, agent?.id]);
|
|
194
|
+
const catalog = useLoad(() => models(target), [], [target]);
|
|
195
|
+
const on = useLoad(() => tools(target, { activated: true }), [], [target]);
|
|
153
196
|
const [d, setD] = useState(EMPTY);
|
|
154
197
|
const [ready, setReady] = useState(!agent);
|
|
155
198
|
const [working, setWorking] = useState(false);
|
|
@@ -168,28 +211,32 @@ function Editor({ agent, onClose, onSaved, onDeleted, }) {
|
|
|
168
211
|
const choices = useMemo(() => {
|
|
169
212
|
const listed = on.value.map((x) => ({ name: x.name, source: x.source, description: x.description }));
|
|
170
213
|
const seen = new Set(listed.map((x) => x.name));
|
|
171
|
-
const kept = d.tools.filter((n) => n !== ALL && !seen.has(n)).map((name) => ({ name, source: 'not on', description: '' }));
|
|
214
|
+
const kept = d.tools.filter((n) => n !== ALL && !seen.has(n)).map((name) => ({ name, source: t('not on'), description: '' }));
|
|
172
215
|
return [...kept, ...listed];
|
|
173
|
-
}, [on.value, d.tools]);
|
|
216
|
+
}, [on.value, d.tools, t]);
|
|
174
217
|
const shownTools = choices.filter((x) => matches(find, x.name, x.source, x.description));
|
|
175
218
|
const save = async () => {
|
|
176
219
|
setWorking(true);
|
|
177
220
|
setNote('');
|
|
178
221
|
try {
|
|
179
222
|
const was = full.value;
|
|
180
|
-
const saved = agent && was ? await update(
|
|
223
|
+
const saved = agent && was ? await update(target, was, d) : await create(target, d);
|
|
181
224
|
onSaved(saved);
|
|
182
225
|
}
|
|
183
226
|
catch (e) {
|
|
184
|
-
setNote(say(e));
|
|
227
|
+
setNote(say(e, t));
|
|
185
228
|
}
|
|
186
229
|
finally {
|
|
187
230
|
setWorking(false);
|
|
188
231
|
}
|
|
189
232
|
};
|
|
190
233
|
const pick = (name, yes) => set({ tools: yes ? [...d.tools, name] : d.tools.filter((n) => n !== name) });
|
|
191
|
-
return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Failed, { error: full.error, onRetry: full.reload })) : !ready ? (_jsx(Soft, { children:
|
|
192
|
-
|
|
234
|
+
return (_jsx(Sheet, { title: agent ? agent.name : t('New agent'), open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Failed, { error: full.error, onRetry: full.reload })) : !ready ? (_jsx(Soft, { children: t('Loading the agent…') })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('Name'), hint: agent ? t('An agent keeps its name.') : t('Letters, digits, . _ or -. Other agents call it as agent_<name>.'), children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), "aria-label": t('Name'), disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: t('Description'), hint: t('The line another agent reads to decide whether to call this one.'), children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: t('Answers questions about our codebase'), "aria-label": t('Description') }) }), _jsx(Field, { label: t('Model'), children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: t('Model'), placeholder: t('The deployment’s default'), models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error ? say(catalog.error, t) : null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: t('Use the default') })) : null] }) }), _jsx(Field, { label: t('Instructions'), hint: t('The system prompt: what the model reads before every run.'), children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: t('Be terse, and cite the file you read.'), "aria-label": t('Instructions'), rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: t('Tools') }), _jsx(SizableText, { size: "$1", color: "$soft", children: every
|
|
235
|
+
? t('Every tool the fleet’s MCP server serves when it runs.')
|
|
236
|
+
: d.tools.length
|
|
237
|
+
? t('{n} chosen. It can call only these.', { n: d.tools.length })
|
|
238
|
+
: t('None chosen: it can call no tool.') })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Every tool') }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": t('Every tool') })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: t('Find a tool…'), "aria-label": t('Find a tool') }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: t('Loading the tools that are on…') })) : on.error && !on.value.length ? (_jsx(Failed, { error: on.error, onRetry: on.reload })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? t('No tool matches.') : t('No tool is on for your organization yet. Add skills or connectors first.') })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": t('Use {name}', { name: x.name }) })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: t('Budget') }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t(EACH[d.period]) }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": t('Budget each period'), inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('One run, USD') }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": t('Budget for one run'), inputMode: "decimal" })] }), _jsx(Choice, { label: t('Period'), value: d.period, options: PERIODS.map((p) => ({ id: p, label: t(PERIOD[p]) })), onChange: (p) => set({ period: p }) })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? t(SO_FAR[full.value.period], { spent: money(full.value.spent) }) : '', t('A run stops at either limit.')].filter(Boolean).join(' ') })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: t('Delete') })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? t('Save') : t('Create') })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: t('The agent and every run recorded against it are removed.'), open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
239
|
+
await remove(target, agent.id || agent.name);
|
|
193
240
|
onDeleted();
|
|
194
241
|
} })) : null] })) }));
|
|
195
242
|
}
|
|
@@ -11,8 +11,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* all its tools on its card and one per tool inside; a tool is called only
|
|
12
12
|
* while it is on. Adding a server switches its tools on. A server's secret is
|
|
13
13
|
* sealed in KMS and never shown again.
|
|
14
|
+
*
|
|
15
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a server's name,
|
|
16
|
+
* address, description and tools are the registry's or the org's, drawn as given.
|
|
14
17
|
*/
|
|
15
18
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
19
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
16
20
|
import { ChevronDown, ExternalLink, KeyRound, Server as ServerIcon } from '@hanzogui/lucide-icons-2';
|
|
17
21
|
import { Button, Input, Switch } from '@hanzo/ui';
|
|
18
22
|
import { useEffect, useMemo, useState } from 'react';
|
|
@@ -24,19 +28,20 @@ import { Out } from '../out.js';
|
|
|
24
28
|
import { route } from '../route.js';
|
|
25
29
|
import { useLoad } from './load.js';
|
|
26
30
|
import { say } from './say.js';
|
|
27
|
-
import { Add, Confirm, Empty, Failed, Featured, Field, Grid,
|
|
31
|
+
import { Add, Confirm, Empty, Failed, Featured, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
|
|
28
32
|
const PAGE = 48;
|
|
29
33
|
/** The part of a URL a person reads: its host and path. */
|
|
30
34
|
const where = (url) => url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
31
35
|
export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
|
|
36
|
+
const { t } = useI18n();
|
|
32
37
|
const host = useHost();
|
|
33
|
-
const
|
|
38
|
+
const target = useTarget();
|
|
34
39
|
const signed = Boolean(host.person);
|
|
35
40
|
// What every run in the org connects to is an org admin's to change; a member reads it.
|
|
36
41
|
const may = signed && host.admin;
|
|
37
|
-
const mine = useLoad(signed ? () => servers(
|
|
42
|
+
const mine = useLoad(signed ? () => servers(target) : null, [], [target, signed]);
|
|
38
43
|
// 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(
|
|
44
|
+
const plane = useLoad(signed && view === 'yours' && mine.value.length ? () => tools(target, { source: 'mcp' }) : null, [], [target, signed, view, mine.value]);
|
|
40
45
|
const [opened, setOpened] = useState(null);
|
|
41
46
|
const [listing, setListing] = useState(null);
|
|
42
47
|
const [connecting, setConnecting] = useState(null);
|
|
@@ -51,12 +56,12 @@ export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
|
|
|
51
56
|
setBusy(s.id);
|
|
52
57
|
setNote('');
|
|
53
58
|
try {
|
|
54
|
-
await toggle(
|
|
59
|
+
await toggle(target, next ? its : [], next ? [] : its);
|
|
55
60
|
plane.reload();
|
|
56
|
-
setNote(next ?
|
|
61
|
+
setNote(next ? t('{name} is on', { name: s.name }) : t('{name} is off', { name: s.name }));
|
|
57
62
|
}
|
|
58
63
|
catch (e) {
|
|
59
|
-
setNote(say(e));
|
|
64
|
+
setNote(say(e, t));
|
|
60
65
|
}
|
|
61
66
|
finally {
|
|
62
67
|
setBusy('');
|
|
@@ -80,40 +85,41 @@ export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
|
|
|
80
85
|
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Line, { children: note }), _jsx(Shelf, { q: q, signed: signed, may: may, added: new Set(mine.value.map((s) => s.listing).filter(Boolean)), onOpen: setListing, onAdd: setConnecting }), _jsx(Natives, { q: q }), detail, connect] }));
|
|
81
86
|
}
|
|
82
87
|
if (!signed) {
|
|
83
|
-
return _jsx(Visitor, { children:
|
|
88
|
+
return _jsx(Visitor, { children: t('Sign in to see your connectors.') });
|
|
84
89
|
}
|
|
85
90
|
const shown = mine.value.filter((s) => matches(q, s.name, s.url, s.listing));
|
|
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:
|
|
91
|
+
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: t('Loading your connectors…') })) : !mine.value.length ? (_jsxs(Empty, { title: t('No connectors yet'), detail: may ? t('Connect an app from Browse, or add any MCP server by its URL.') : t('An org admin adds connectors. Browse what there is.'), children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: t('Browse connectors') }), may ? (_jsx(Button, { size: "sm", onPress: () => onAdding(true), children: t('Add by URL') })) : null] })) : !shown.length ? (_jsx(Soft, { children: t('No connector of yours matches “{q}”.', { q: q.trim() }) })) : (_jsx(Grid, { label: t('Your connectors'), children: shown.map((s) => {
|
|
87
92
|
const its = plane.value.filter((x) => owns(s, x.name));
|
|
88
93
|
const lit = its.filter((x) => x.activated).length;
|
|
89
94
|
return (_jsx(Tile, { title: s.name, detail: where(s.url), meta: [
|
|
90
|
-
its.length ?
|
|
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',
|
|
95
|
+
its.length ? t('{on} of {n} tools on', { on: lit, n: its.length }) : plane.loading ? '' : t('No tools listed yet'),
|
|
96
|
+
s.listing ? t('From Browse') : t('Added by URL'),
|
|
97
|
+
s.secret ? t('Secret in KMS') : '',
|
|
98
|
+
s.admitted ? '' : t('Not in runs until an admin adds it again'),
|
|
94
99
|
]
|
|
95
100
|
.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":
|
|
97
|
-
}) })), plane.error && mine.value.length ? _jsx(Line, { children:
|
|
101
|
+
.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": t('{name} on', { name: s.name }) })) : its.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: lit ? t('On') : t('Off') })) : undefined }, s.id));
|
|
102
|
+
}) })), plane.error && mine.value.length ? _jsx(Line, { children: t('Tools: {reason}', { reason: say(plane.error, t) }) }) : null, opened ? (_jsx(Detail, { server: opened, may: may, onClose: () => {
|
|
98
103
|
setOpened(null);
|
|
99
104
|
plane.reload();
|
|
100
105
|
}, onDeleted: () => {
|
|
101
|
-
setNote(
|
|
106
|
+
setNote(t('{name} is removed', { name: opened.name }));
|
|
102
107
|
setOpened(null);
|
|
103
108
|
mine.reload();
|
|
104
109
|
} })) : null, connect] }));
|
|
105
110
|
}
|
|
106
111
|
/** The shelf: featured first, then by name, a page at a time. The platform searches it. */
|
|
107
112
|
function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
108
|
-
const t =
|
|
113
|
+
const { t } = useI18n();
|
|
114
|
+
const target = useTarget();
|
|
109
115
|
const text = useSettled(q.trim(), 300);
|
|
110
|
-
const first = useLoad(signed ? () => shelf(
|
|
116
|
+
const first = useLoad(signed ? () => shelf(target, { text, limit: PAGE }) : null, null, [target, signed, text]);
|
|
111
117
|
const [more, setMore] = useState([]);
|
|
112
118
|
const [paging, setPaging] = useState(false);
|
|
113
119
|
const [note, setNote] = useState('');
|
|
114
120
|
useEffect(() => setMore([]), [first.value]);
|
|
115
121
|
if (!signed) {
|
|
116
|
-
return (_jsx(Part, { title:
|
|
122
|
+
return (_jsx(Part, { title: t('Connect an app'), detail: t('Give runs the tools you already use.'), children: _jsx(Visitor, { children: t('Sign in to browse connectors.') }) }));
|
|
117
123
|
}
|
|
118
124
|
const page = first.value;
|
|
119
125
|
const list = [...(page?.listings ?? []), ...more];
|
|
@@ -125,20 +131,20 @@ function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
|
125
131
|
setPaging(true);
|
|
126
132
|
setNote('');
|
|
127
133
|
try {
|
|
128
|
-
const got = await shelf(
|
|
134
|
+
const got = await shelf(target, { text, limit: PAGE, offset: list.length });
|
|
129
135
|
setMore((m) => [...m, ...got.listings]);
|
|
130
136
|
}
|
|
131
137
|
catch (e) {
|
|
132
|
-
setNote(say(e));
|
|
138
|
+
setNote(say(e, t));
|
|
133
139
|
}
|
|
134
140
|
finally {
|
|
135
141
|
setPaging(false);
|
|
136
142
|
}
|
|
137
143
|
};
|
|
138
|
-
return (_jsx(Part, { title:
|
|
144
|
+
return (_jsx(Part, { title: t('Connect an app'), detail: t('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: t('Loading connectors…') })) : !list.length ? (_jsx(Soft, { children: text ? t('No connector matches “{q}”.', { q: text }) : t('There are no connectors to add yet.') })) : (_jsxs(YStack, { gap: "$3", children: [hero ? (_jsx(Featured, { title: titleOf(hero), detail: hero.description, meta: facts(hero, t), 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 ? t('{shown} of {n} connectors', { shown: list.length, n: page.total }) : t('{n} connectors', { n: page.total }) }), _jsx(Grid, { label: t('Servers on the shelf'), children: rest.map((l) => (_jsx(Tile, { title: titleOf(l), detail: l.description, meta: facts(l, t), 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: t('Show more') }) })) : null] })) }));
|
|
139
145
|
}
|
|
140
|
-
/** The facts a card carries under its description. */
|
|
141
|
-
const facts = (l) => [l.vendor, l.official ? 'Official' : '', ready(l) ? 'Hosted' : 'Package only: needs a place to run'].filter(Boolean).join(' · ');
|
|
146
|
+
/** The facts a card carries under its description, said in the reader's language. */
|
|
147
|
+
const facts = (l, t) => [l.vendor, l.official ? t('Official') : '', ready(l) ? t('Hosted') : t('Package only: needs a place to run')].filter(Boolean).join(' · ');
|
|
142
148
|
/** `value` once it has held still for `ms`. */
|
|
143
149
|
function useSettled(value, ms) {
|
|
144
150
|
const [settled, setSettled] = useState(value);
|
|
@@ -154,33 +160,36 @@ function useSettled(value, ms) {
|
|
|
154
160
|
* at once on the old MCP address, which exists to show them.
|
|
155
161
|
*/
|
|
156
162
|
function Natives({ q }) {
|
|
163
|
+
const { t } = useI18n();
|
|
157
164
|
const host = useHost();
|
|
158
|
-
const
|
|
165
|
+
const target = useTarget();
|
|
159
166
|
const r = route(host.path);
|
|
160
167
|
const [asked, setAsked] = useState(r.kind === 'screen' && r.screen === 'mcp');
|
|
161
|
-
const list = useLoad(asked ? () => natives(
|
|
168
|
+
const list = useLoad(asked ? () => natives(target) : null, [], [target, asked]);
|
|
162
169
|
const [open, setOpen] = useState(null);
|
|
163
170
|
const shown = list.value.filter((s) => matches(q, s.name, s.description, ...s.ops));
|
|
164
171
|
const ops = shown.reduce((n, s) => n + s.ops.length, 0);
|
|
165
|
-
return (_jsx(Part, { title:
|
|
172
|
+
return (_jsx(Part, { title: t('Built in'), detail: t('Hanzo’s 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: t('Show built-in servers') })), children: !asked ? null : list.loading && !list.value.length ? (_jsx(Soft, { children: t('Loading built-in servers…') })) : list.error && !list.value.length ? (_jsx(Failed, { error: list.error, onRetry: list.reload })) : !shown.length ? (_jsx(Soft, { children: list.value.length ? t('No built-in server matches “{q}”.', { q: q.trim() }) : t('No built-in servers are listed.') })) : (_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('{servers} servers · {ops} operations', { servers: shown.length, ops }) }), shown.map((s) => {
|
|
166
173
|
const on = open === s.name;
|
|
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' },
|
|
174
|
+
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' }, "$platform-web": { textAlign: 'start' }, 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: t('This server lists no operations.') })) })) : null] }, s.name));
|
|
168
175
|
})] })) }));
|
|
169
176
|
}
|
|
170
177
|
/** One listing in full: what it is, where it is served, and adding it. */
|
|
171
178
|
function About({ listing, added, signed, onAdd, onClose }) {
|
|
172
|
-
const t =
|
|
173
|
-
const
|
|
179
|
+
const { t } = useI18n();
|
|
180
|
+
const target = useTarget();
|
|
181
|
+
const full = useLoad(() => one(target, listing.id), listing, [target, listing.id]);
|
|
174
182
|
const l = full.value;
|
|
175
183
|
const links = [l.site, l.repo].filter((u) => u.startsWith('https://'));
|
|
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:
|
|
184
|
+
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 ? t('Official') : ''].filter(Boolean).join(' · ') })] })] }), l.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: l.description })) : null, l.remotes.length ? (_jsx(Field, { label: t('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: t('Packages'), hint: ready(l) ? undefined : t('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: t('Showing what Browse listed. {reason}', { reason: say(full.error, t) }) }) : null, signed && ready(l) ? (_jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", disabled: added, onPress: onAdd, children: added ? t('Added') : t('Add to your connectors') }) })) : null] }));
|
|
177
185
|
}
|
|
178
186
|
/**
|
|
179
187
|
* Add a server: a listing off the shelf, or any MCP URL. The secret, when it
|
|
180
188
|
* needs one, is sealed in KMS as it is added; then its tools are switched on.
|
|
181
189
|
*/
|
|
182
190
|
function Connect({ listing, onClose, onAdded }) {
|
|
183
|
-
const t =
|
|
191
|
+
const { t } = useI18n();
|
|
192
|
+
const target = useTarget();
|
|
184
193
|
const [name, setName] = useState('');
|
|
185
194
|
const [url, setUrl] = useState('');
|
|
186
195
|
const [header, setHeader] = useState('Authorization');
|
|
@@ -192,32 +201,33 @@ function Connect({ listing, onClose, onAdded }) {
|
|
|
192
201
|
setNote('');
|
|
193
202
|
const a = listing ? { listing: listing.id, name, header, secret } : { name, url, header, secret };
|
|
194
203
|
try {
|
|
195
|
-
const s = await add(
|
|
196
|
-
let said =
|
|
204
|
+
const s = await add(target, a);
|
|
205
|
+
let said = t('{name} is added.', { name: s.name });
|
|
197
206
|
try {
|
|
198
|
-
const its = (await tools(
|
|
207
|
+
const its = (await tools(target, { source: 'mcp' })).filter((x) => owns(s, x.name)).map((x) => x.name);
|
|
199
208
|
if (its.length)
|
|
200
|
-
await toggle(
|
|
201
|
-
said = its.length ?
|
|
209
|
+
await toggle(target, its);
|
|
210
|
+
said = its.length ? t('{name} is added, and its {n} tools are on.', { name: s.name, n: its.length }) : t('{name} is added. It listed no tools yet.', { name: s.name });
|
|
202
211
|
}
|
|
203
212
|
catch (e) {
|
|
204
|
-
said =
|
|
213
|
+
said = t('{name} is added, but its tools could not be switched on. {reason}', { name: s.name, reason: say(e, t) });
|
|
205
214
|
}
|
|
206
215
|
onAdded(said);
|
|
207
216
|
}
|
|
208
217
|
catch (e) {
|
|
209
|
-
setNote(say(e));
|
|
218
|
+
setNote(say(e, t));
|
|
210
219
|
}
|
|
211
220
|
finally {
|
|
212
221
|
setWorking(false);
|
|
213
222
|
}
|
|
214
223
|
};
|
|
215
|
-
return (_jsxs(Sheet, { title: listing ?
|
|
224
|
+
return (_jsxs(Sheet, { title: listing ? t('Add {name}', { name: titleOf(listing) }) : t('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: t('Any MCP server that speaks HTTP. Its tools are on once it is added.') })), _jsx(Field, { label: t('Name'), hint: listing ? t('Leave it empty to use the listing’s own.') : undefined, children: _jsx(Input, { value: name, onChangeText: setName, placeholder: listing ? titleOf(listing) : t('Docs'), "aria-label": t('Name') }) }), listing ? null : (_jsx(Field, { label: "URL", hint: t('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: t('Secret'), hint: t('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": t('Secret'), secureTextEntry: true }) }), secret ? (_jsx(Field, { label: t('Header'), children: _jsx(Input, { value: header, onChangeText: setHeader, "aria-label": t('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: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: t('Add') })] })] }));
|
|
216
225
|
}
|
|
217
226
|
/** One of the org's servers: where it is, its secret, its tools and their switches, and removing it. */
|
|
218
227
|
function Detail({ server, may, onClose, onDeleted }) {
|
|
219
|
-
const t =
|
|
220
|
-
const
|
|
228
|
+
const { t } = useI18n();
|
|
229
|
+
const target = useTarget();
|
|
230
|
+
const list = useLoad(() => tools(target, { source: 'mcp' }), [], [target, server.id]);
|
|
221
231
|
const its = useMemo(() => list.value.filter((x) => owns(server, x.name)), [list.value, server]);
|
|
222
232
|
const [busy, setBusy] = useState(false);
|
|
223
233
|
const [note, setNote] = useState('');
|
|
@@ -227,21 +237,25 @@ function Detail({ server, may, onClose, onDeleted }) {
|
|
|
227
237
|
setBusy(true);
|
|
228
238
|
setNote('');
|
|
229
239
|
try {
|
|
230
|
-
await toggle(
|
|
240
|
+
await toggle(target, on, offNames);
|
|
231
241
|
list.reload();
|
|
232
242
|
}
|
|
233
243
|
catch (e) {
|
|
234
|
-
setNote(say(e));
|
|
244
|
+
setNote(say(e, t));
|
|
235
245
|
}
|
|
236
246
|
finally {
|
|
237
247
|
setBusy(false);
|
|
238
248
|
}
|
|
239
249
|
};
|
|
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
|
|
250
|
+
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
|
|
251
|
+
? server.header
|
|
252
|
+
? t('A secret is sealed in KMS and sent in {header}.', { header: server.header })
|
|
253
|
+
: t('A secret is sealed in KMS and sent in its header.')
|
|
254
|
+
: t('No secret: it is called as it is.') })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", flex: 1, children: t('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 ? t('Turn all on') : t('Turn all off') })) : null] }), list.loading && !list.value.length ? (_jsx(Soft, { children: t('Loading its tools…') })) : list.error && !list.value.length ? (_jsx(Failed, { error: list.error, onRetry: list.reload })) : !its.length ? (_jsx(Soft, { children: t('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) => {
|
|
241
255
|
const short = x.name.slice(server.id.length + 1);
|
|
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":
|
|
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:
|
|
244
|
-
await remove(
|
|
256
|
+
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": t('{name} on', { name: short }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.activated ? t('On') : t('Off') }))] }, x.name));
|
|
257
|
+
}) })), _jsx(Line, { children: note || (may ? '' : t('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: t('Remove connector') }) })) : null, _jsx(Confirm, { what: server.name, says: t('Its tools leave your organization and every agent, and its sealed secret is destroyed.'), open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
258
|
+
await remove(target, server.id);
|
|
245
259
|
onDeleted();
|
|
246
260
|
} })] }));
|
|
247
261
|
}
|