@hanzo/build 0.2.10 → 0.2.12
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 +98 -25
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/coding.d.ts +31 -1
- package/lib/api/coding.js +50 -1
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sessions.d.ts +32 -4
- package/lib/api/sessions.js +42 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +221 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +199 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +52 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +127 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +4 -1
- package/lib/desk.js +3 -16
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +4 -4
- package/lib/git.d.ts +4 -0
- package/lib/git.js +6 -2
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +30 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +184 -32
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
package/lib/builder.js
CHANGED
|
@@ -11,12 +11,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* /-/issues the forge's issues
|
|
12
12
|
* /-/artifacts what this org has built
|
|
13
13
|
* /-/templates the public starters
|
|
14
|
-
* /-/
|
|
14
|
+
* /-/customize skills, connectors, plugins and agents
|
|
15
15
|
* /<slug> a deployed project's workspace, in this same window
|
|
16
16
|
*
|
|
17
17
|
* The rail is the sessions rail every Hanzo surface shares: New, the builder's
|
|
18
|
-
* places, the org's coding runs newest first with a
|
|
19
|
-
*
|
|
18
|
+
* places with the rest under More, the org's coding runs newest first with a
|
|
19
|
+
* live status dot, and at the foot the offer of Hanzo in Slack and the account,
|
|
20
|
+
* whose menu opens Settings, Usage, the plans, help and logging out (foot.tsx).
|
|
21
|
+
* Its collapse is an explicit toggle kept per browser.
|
|
20
22
|
*
|
|
21
23
|
* A project's workspace takes the whole window — it has its own chat column,
|
|
22
24
|
* bar and panes — and its mark leads back here.
|
|
@@ -26,22 +28,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
26
28
|
* left column, never two.
|
|
27
29
|
*/
|
|
28
30
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
29
|
-
import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu,
|
|
31
|
+
import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
|
|
30
32
|
import { Button } from '@hanzo/ui';
|
|
31
33
|
import { SessionRail } from '@hanzo/ui/chat';
|
|
32
34
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
33
|
-
import {
|
|
35
|
+
import { useMemo, useState } from 'react';
|
|
34
36
|
import { pinBoard } from './choice.js';
|
|
37
|
+
import { Customize } from './customize/index.js';
|
|
35
38
|
import { useKept, useRecents } from './data.js';
|
|
36
39
|
import { Forge } from './forge.js';
|
|
37
40
|
import { HostProvider, useHost, useTarget } from './host.js';
|
|
38
41
|
import { Landing } from './landing.js';
|
|
42
|
+
import { PrefsProvider } from './prefs.js';
|
|
39
43
|
import { Project } from './project.js';
|
|
44
|
+
import { Plans } from './plans.js';
|
|
40
45
|
import { path, route } from './route.js';
|
|
41
46
|
import { Run } from './run.js';
|
|
42
47
|
import { DOTS } from './section.js';
|
|
43
48
|
import { Artifacts, Templates } from './shelf.js';
|
|
44
|
-
import {
|
|
49
|
+
import { Find } from './find.js';
|
|
50
|
+
import { DOCS, Slack, Who } from './foot.js';
|
|
51
|
+
import { Settings } from './settings/index.js';
|
|
45
52
|
import { Where } from './switch.js';
|
|
46
53
|
function Shell() {
|
|
47
54
|
const host = useHost();
|
|
@@ -51,9 +58,9 @@ function Shell() {
|
|
|
51
58
|
const [collapsed, setCollapsed] = useKept('hanzo.build.rail', false);
|
|
52
59
|
const [drawer, setDrawer] = useState(false);
|
|
53
60
|
const [order, setOrder] = useState('newest');
|
|
54
|
-
const [account, setAccount] = useState(false);
|
|
55
61
|
const [finding, setFinding] = useState(false);
|
|
56
|
-
|
|
62
|
+
const [menu, setMenu] = useState(false);
|
|
63
|
+
const [slack, setSlack] = useKept('hanzo.build.slack', false);
|
|
57
64
|
const go = (p) => {
|
|
58
65
|
setDrawer(false);
|
|
59
66
|
host.go(p);
|
|
@@ -70,9 +77,13 @@ function Shell() {
|
|
|
70
77
|
const switcher = Boolean(host.person && (host.org || (host.memberships?.length ?? 0) > 0));
|
|
71
78
|
const top = switcher ? (_jsx(Where, {})) : (_jsx(XStack, { render: "button", "aria-label": "Hanzo", items: "center", onPress: () => go(''), children: _jsx(HanzoMark, { size: 18 }) }));
|
|
72
79
|
const links = [
|
|
80
|
+
{ id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
|
|
81
|
+
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
82
|
+
{ id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: () => go(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
|
|
73
83
|
{ id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' },
|
|
84
|
+
];
|
|
85
|
+
const more = [
|
|
74
86
|
{ id: 'codebases', label: 'Codebase', icon: _jsx(FolderGit2, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), active: screen === 'codebases' },
|
|
75
|
-
{ id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
|
|
76
87
|
{
|
|
77
88
|
id: 'issues',
|
|
78
89
|
label: 'Issues',
|
|
@@ -83,22 +94,25 @@ function Shell() {
|
|
|
83
94
|
},
|
|
84
95
|
active: screen === 'issues',
|
|
85
96
|
},
|
|
86
|
-
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
87
|
-
{ id: 'mcp', label: 'MCP', icon: _jsx(Puzzle, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), active: screen === 'mcp' },
|
|
88
|
-
];
|
|
89
|
-
const more = [
|
|
90
97
|
{ id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
|
|
91
98
|
{
|
|
92
99
|
id: 'machines',
|
|
93
100
|
label: 'Machines',
|
|
94
101
|
icon: _jsx(Cpu, { size: 16 }),
|
|
95
|
-
onPress: () =>
|
|
102
|
+
onPress: () => go(path({ kind: 'settings', section: 'machines' })),
|
|
103
|
+
active: r.kind === 'settings' && r.section === 'machines',
|
|
96
104
|
},
|
|
97
|
-
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(
|
|
105
|
+
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
|
|
98
106
|
];
|
|
99
|
-
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
|
|
100
|
-
? {
|
|
101
|
-
|
|
107
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), notice: host.person && !slack ? (_jsx(Slack, { onOpen: () => go(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) })) : undefined, account: host.person
|
|
108
|
+
? {
|
|
109
|
+
name: host.person.email || host.person.name,
|
|
110
|
+
onPress: () => {
|
|
111
|
+
setDrawer(false);
|
|
112
|
+
setMenu(true);
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => go(path({ kind: 'settings', section: 'general' })), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : switcher ? 80 : 40, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsx(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", "$max-md": { display: 'none' }, children: top })), host.person ? _jsx(Who, { open: menu, onOpenChange: setMenu, narrow: collapsed }) : null] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 44, px: "$2", gap: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }), top] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
|
|
102
116
|
}
|
|
103
117
|
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
104
118
|
function Pane({ onStarted }) {
|
|
@@ -106,11 +120,17 @@ function Pane({ onStarted }) {
|
|
|
106
120
|
const r = route(host.path);
|
|
107
121
|
if (r.kind === 'run')
|
|
108
122
|
return _jsx(Run, { id: r.id }, r.id);
|
|
123
|
+
if (r.kind === 'settings')
|
|
124
|
+
return _jsx(Settings, { section: r.section });
|
|
125
|
+
if (r.kind === 'customize')
|
|
126
|
+
return _jsx(Customize, { tab: r.tab });
|
|
109
127
|
if (r.kind === 'screen') {
|
|
110
128
|
if (r.screen === 'artifacts')
|
|
111
129
|
return _jsx(Artifacts, {});
|
|
112
130
|
if (r.screen === 'templates')
|
|
113
131
|
return _jsx(Templates, {});
|
|
132
|
+
if (r.screen === 'plans')
|
|
133
|
+
return _jsx(Plans, {});
|
|
114
134
|
return _jsx(Forge, { screen: r.screen }, r.screen);
|
|
115
135
|
}
|
|
116
136
|
return (_jsx(Landing, { onStarted: (id) => {
|
|
@@ -139,5 +159,5 @@ function Screens({ rail }) {
|
|
|
139
159
|
* places and runs in its own rail with `DevSection`.
|
|
140
160
|
*/
|
|
141
161
|
export function Builder({ host, rail = true }) {
|
|
142
|
-
return (_jsx(HostProvider, { host: host, children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: "$background", children: _jsx(Screens, { rail: rail }) }) }));
|
|
162
|
+
return (_jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: "$background", children: _jsx(Screens, { rail: rail }) }) }) }));
|
|
143
163
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Agents: the org's own — a model, instructions, the tools it may call, and
|
|
4
|
+
* what it may spend.
|
|
5
|
+
*
|
|
6
|
+
* Yours lists them; opening one edits it in place, and only what changed is
|
|
7
|
+
* sent. An agent calls only the tools it names, and only those that are on for
|
|
8
|
+
* the org, so the choices are the tools that are on; "every tool" is whatever
|
|
9
|
+
* the fleet's MCP server serves when it runs. A budget is required: what it may
|
|
10
|
+
* spend each period, and in one run. Discover is the platform's presets whose
|
|
11
|
+
* tool calls run on the platform; adding one opens a new agent written from it.
|
|
12
|
+
*/
|
|
13
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
|
+
import { Bot } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { Button, Input, Switch, Textarea } from '@hanzo/ui';
|
|
16
|
+
import { ChipSelect } from '@hanzo/ui/product';
|
|
17
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
18
|
+
import { agents, ALL, create, draft, EMPTY, fromPreset, one, PERIODS, presets, remove, update } from '../api/agents.js';
|
|
19
|
+
import { label as named, models } from '../api/models.js';
|
|
20
|
+
import { tools } from '../api/tools.js';
|
|
21
|
+
import { useRead } from '../data.js';
|
|
22
|
+
import { useHost, useTarget } from '../host.js';
|
|
23
|
+
import { Add, Choice, Confirm, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile } from './ui.js';
|
|
24
|
+
/** The model a new agent runs on when none is chosen: the deployment's own default. */
|
|
25
|
+
const DEFAULT = 'default';
|
|
26
|
+
const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '') || '0'}`;
|
|
27
|
+
export function Agents({ view, q, adding, onAdding, onView }) {
|
|
28
|
+
const host = useHost();
|
|
29
|
+
const t = useTarget();
|
|
30
|
+
const signed = Boolean(host.person);
|
|
31
|
+
const mine = useRead(signed ? () => agents(t) : null, [], [t, signed]);
|
|
32
|
+
const offered = useRead(view === 'discover' ? () => presets(t) : null, [], [t, view]);
|
|
33
|
+
const [opened, setOpened] = useState(null);
|
|
34
|
+
const [start, setStart] = useState(null);
|
|
35
|
+
const [note, setNote] = useState('');
|
|
36
|
+
const closeNew = () => {
|
|
37
|
+
setStart(null);
|
|
38
|
+
onAdding(false);
|
|
39
|
+
};
|
|
40
|
+
const editor = adding || start ? (_jsx(Editor, { start: start ?? EMPTY, onClose: closeNew, onSaved: (a) => {
|
|
41
|
+
closeNew();
|
|
42
|
+
onView('yours');
|
|
43
|
+
mine.reload();
|
|
44
|
+
setNote(`${a.name} is saved`);
|
|
45
|
+
} })) : opened ? (_jsx(Editor, { agent: opened, onClose: () => setOpened(null), onSaved: (a) => {
|
|
46
|
+
setOpened(null);
|
|
47
|
+
mine.reload();
|
|
48
|
+
setNote(`${a.name} is saved`);
|
|
49
|
+
}, onDeleted: () => {
|
|
50
|
+
setNote(`${opened.name} is deleted`);
|
|
51
|
+
setOpened(null);
|
|
52
|
+
mine.reload();
|
|
53
|
+
} })) : null;
|
|
54
|
+
if (view === 'discover') {
|
|
55
|
+
const names = new Set(mine.value.map((a) => a.name));
|
|
56
|
+
const shown = offered.value.filter((p) => matches(q, p.id, p.title, p.prompt));
|
|
57
|
+
return (_jsxs(YStack, { gap: "$3", children: [offered.error && !offered.value.length ? (_jsx(Soft, { children: offered.error.message })) : offered.loading && !offered.value.length ? (_jsx(Soft, { children: "Reading the presets\u2026" })) : !shown.length ? (_jsx(Soft, { children: offered.value.length ? 'No preset matches.' : 'The platform offers no presets.' })) : (_jsxs(_Fragment, { children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Presets from the platform. Adding one opens a new agent written from it, to change before it is saved." }), _jsx(Grid, { label: "Presets", children: shown.map((p) => (_jsx(Tile, { title: p.title || p.id, detail: p.prompt, meta: `Preset ${p.id}`, mark: _jsx(Mark, { name: p.title || p.id, icon: _jsx(Bot, { size: 15 }) }), action: signed ? _jsx(Add, { name: p.title || p.id, added: names.has(p.id), onPress: () => setStart(fromPreset(p)) }) : undefined }, p.id))) })] })), editor] }));
|
|
58
|
+
}
|
|
59
|
+
if (!signed) {
|
|
60
|
+
return _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your agents." });
|
|
61
|
+
}
|
|
62
|
+
const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
|
|
63
|
+
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Soft, { children: mine.error.message })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Reading your agents\u2026" })) : !mine.value.length ? (_jsx(Soft, { action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children: "New agent" }), children: "No agents yet. Give one a model, instructions and the tools it may call." })) : !shown.length ? (_jsx(Soft, { children: "No agent matches." })) : (_jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Mark, { name: a.name, icon: a.emoji ? _jsx(SizableText, { size: "$4", children: a.emoji }) : undefined }), onOpen: () => setOpened(a) }, a.id || a.name))) })), editor] }));
|
|
64
|
+
function reach(a) {
|
|
65
|
+
if (a.tools.includes(ALL))
|
|
66
|
+
return 'every tool';
|
|
67
|
+
return a.tools.length === 1 ? '1 tool' : `${a.tools.length} tools`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Write a new agent, or change one; an existing agent is read whole first, for its instructions. */
|
|
71
|
+
function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
|
|
72
|
+
const t = useTarget();
|
|
73
|
+
const full = useRead(agent ? () => one(t, agent.id || agent.name) : null, null, [t, agent?.id]);
|
|
74
|
+
const catalog = useRead(() => models(t), [], [t]);
|
|
75
|
+
const on = useRead(() => tools(t, { activated: true }), [], [t]);
|
|
76
|
+
const [d, setD] = useState(start ?? EMPTY);
|
|
77
|
+
const [ready, setReady] = useState(!agent);
|
|
78
|
+
const [working, setWorking] = useState(false);
|
|
79
|
+
const [note, setNote] = useState('');
|
|
80
|
+
const [asking, setAsking] = useState(false);
|
|
81
|
+
const [find, setFind] = useState('');
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (full.value) {
|
|
84
|
+
setD(draft(full.value));
|
|
85
|
+
setReady(true);
|
|
86
|
+
}
|
|
87
|
+
}, [full.value]);
|
|
88
|
+
const set = (p) => setD((x) => ({ ...x, ...p }));
|
|
89
|
+
const every = d.tools.includes(ALL);
|
|
90
|
+
// The tools that are on, and any the agent already names that are not.
|
|
91
|
+
const choices = useMemo(() => {
|
|
92
|
+
const listed = on.value.map((x) => ({ name: x.name, source: x.source, description: x.description }));
|
|
93
|
+
const seen = new Set(listed.map((x) => x.name));
|
|
94
|
+
const kept = d.tools.filter((n) => n !== ALL && !seen.has(n)).map((name) => ({ name, source: 'not on', description: '' }));
|
|
95
|
+
return [...kept, ...listed];
|
|
96
|
+
}, [on.value, d.tools]);
|
|
97
|
+
const shownTools = choices.filter((x) => matches(find, x.name, x.source, x.description));
|
|
98
|
+
const items = useMemo(() => {
|
|
99
|
+
const list = [{ id: DEFAULT, label: 'Default', hint: 'the deployment’s' }, ...catalog.value.map((m) => ({ id: m.id, label: m.label }))];
|
|
100
|
+
if (d.model && !list.some((m) => m.id === d.model))
|
|
101
|
+
list.splice(1, 0, { id: d.model, label: named(d.model) });
|
|
102
|
+
return agent ? list.filter((m) => m.id !== DEFAULT) : list;
|
|
103
|
+
}, [catalog.value, d.model, agent]);
|
|
104
|
+
const chosen = items.find((m) => m.id === (d.model || DEFAULT)) ?? null;
|
|
105
|
+
const save = async () => {
|
|
106
|
+
setWorking(true);
|
|
107
|
+
setNote('');
|
|
108
|
+
try {
|
|
109
|
+
const was = full.value;
|
|
110
|
+
const saved = agent && was ? await update(t, was, d) : await create(t, d);
|
|
111
|
+
onSaved(saved);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
setNote(e instanceof Error ? e.message : 'The agent was not saved');
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
setWorking(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const pick = (name, yes) => set({ tools: yes ? [...d.tools, name] : d.tools.filter((n) => n !== name) });
|
|
121
|
+
return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "helper", "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ChipSelect, { name: "Model", label: chosen?.label ?? 'Default', chosen: chosen, items: items, onChange: (m) => set({ model: m.id === DEFAULT ? '' : m.id }), placeholder: "Search models\u2026", placement: "bottom-start", loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "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: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Reading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Soft, { children: on.error.message })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization. Switch skills and connectors on 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": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
122
|
+
await remove(t, agent.id || agent.name);
|
|
123
|
+
onDeleted();
|
|
124
|
+
} })) : null] })) }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Connectors: MCP servers whose tools an agent calls.
|
|
4
|
+
*
|
|
5
|
+
* Yours is the servers the org added — by URL, or off the shelf — each with the
|
|
6
|
+
* tools it brings and a switch for each; a tool is called only while it is on.
|
|
7
|
+
* Adding a server switches its tools on. Discover is the shelf (our copy of the
|
|
8
|
+
* public MCP registry) and the fleet's own servers, which every run can call and
|
|
9
|
+
* nothing adds. A listing is added here only when it serves streamable HTTP; one
|
|
10
|
+
* that ships only a package needs somewhere to run it first, so it has no plus.
|
|
11
|
+
* A server's secret is sealed in KMS and never shown again.
|
|
12
|
+
*/
|
|
13
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
|
+
import { ChevronDown, ExternalLink, KeyRound, Server as ServerIcon } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { Button, Input, Switch } from '@hanzo/ui';
|
|
16
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
17
|
+
import { add, one, owns, ready, remove, servers, shelf, titleOf } from '../api/connectors.js';
|
|
18
|
+
import { natives } from '../api/mcp.js';
|
|
19
|
+
import { toggle, tools } from '../api/tools.js';
|
|
20
|
+
import { useRead } from '../data.js';
|
|
21
|
+
import { useHost, useTarget } from '../host.js';
|
|
22
|
+
import { Out } from '../out.js';
|
|
23
|
+
import { Add, Confirm, day, Featured, Field, Grid, left, Line, Mark, matches, mono, Part, Sheet, Soft, Tile } from './ui.js';
|
|
24
|
+
const PAGE = 48;
|
|
25
|
+
/** The part of a URL a person reads: its host and path. */
|
|
26
|
+
const where = (url) => url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
27
|
+
export function Connectors({ view, q, adding, onAdding, onView }) {
|
|
28
|
+
const host = useHost();
|
|
29
|
+
const t = useTarget();
|
|
30
|
+
const signed = Boolean(host.person);
|
|
31
|
+
// What every run in the org connects to is an org admin's to change; a member reads it.
|
|
32
|
+
const may = signed && host.admin;
|
|
33
|
+
const mine = useRead(signed ? () => servers(t) : null, [], [t, signed]);
|
|
34
|
+
const [opened, setOpened] = useState(null);
|
|
35
|
+
const [listing, setListing] = useState(null);
|
|
36
|
+
const [connecting, setConnecting] = useState(null);
|
|
37
|
+
const [note, setNote] = useState('');
|
|
38
|
+
const connect = adding || connecting ? (_jsx(Connect, { listing: connecting, onClose: () => {
|
|
39
|
+
setConnecting(null);
|
|
40
|
+
onAdding(false);
|
|
41
|
+
}, onAdded: (said) => {
|
|
42
|
+
setConnecting(null);
|
|
43
|
+
onAdding(false);
|
|
44
|
+
mine.reload();
|
|
45
|
+
onView('yours');
|
|
46
|
+
setNote(said);
|
|
47
|
+
} })) : null;
|
|
48
|
+
const detail = listing ? (_jsx(About, { listing: listing, added: mine.value.some((s) => s.listing === listing.id), signed: may, onAdd: () => {
|
|
49
|
+
setConnecting(listing);
|
|
50
|
+
setListing(null);
|
|
51
|
+
}, onClose: () => setListing(null) })) : null;
|
|
52
|
+
if (view === 'discover') {
|
|
53
|
+
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] }));
|
|
54
|
+
}
|
|
55
|
+
if (!signed) {
|
|
56
|
+
return _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your connectors." });
|
|
57
|
+
}
|
|
58
|
+
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(Soft, { children: mine.error.message })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Reading your connectors\u2026" })) : !mine.value.length ? (_jsx(Soft, { action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Discover connectors" }), children: may ? 'No connectors yet. Add an MCP server by its URL, or pick one off the shelf.' : 'No connectors yet. An org admin adds them.' })) : !shown.length ? (_jsx(Soft, { children: "No connector matches." })) : (_jsx(Grid, { label: "Your connectors", children: shown.map((s) => (_jsx(Tile, { title: s.name, detail: where(s.url), meta: [s.listing ? 'From the shelf' : 'Added by URL', s.secret ? 'secret sealed in KMS' : '', day(s.created)].filter(Boolean).join(' · '), mark: _jsx(Mark, { name: s.name }), onOpen: () => setOpened(s) }, s.id))) })), opened ? (_jsx(Detail, { server: opened, may: may, onClose: () => setOpened(null), onDeleted: () => {
|
|
60
|
+
setNote(`${opened.name} is removed`);
|
|
61
|
+
setOpened(null);
|
|
62
|
+
mine.reload();
|
|
63
|
+
} })) : null, connect] }));
|
|
64
|
+
}
|
|
65
|
+
/** The shelf: featured first, then by name, a page at a time. The platform searches it. */
|
|
66
|
+
function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
67
|
+
const t = useTarget();
|
|
68
|
+
const host = useHost();
|
|
69
|
+
const text = useSettled(q.trim(), 300);
|
|
70
|
+
const first = useRead(signed ? () => shelf(t, { text, limit: PAGE }) : null, null, [t, signed, text]);
|
|
71
|
+
const [more, setMore] = useState([]);
|
|
72
|
+
const [paging, setPaging] = useState(false);
|
|
73
|
+
const [note, setNote] = useState('');
|
|
74
|
+
useEffect(() => setMore([]), [first.value]);
|
|
75
|
+
if (!signed) {
|
|
76
|
+
return (_jsx(Part, { title: "The shelf", detail: "MCP servers the public registries publish.", children: _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to browse the shelf." }) }));
|
|
77
|
+
}
|
|
78
|
+
const page = first.value;
|
|
79
|
+
const list = [...(page?.listings ?? []), ...more];
|
|
80
|
+
const hero = !text ? list.find((l) => l.featured) : undefined;
|
|
81
|
+
const rest = hero ? list.filter((l) => l !== hero) : list;
|
|
82
|
+
const action = (l) => !may ? undefined : ready(l) ? (_jsx(Add, { name: titleOf(l), added: added.has(l.id), onPress: () => onAdd(l) })) : (_jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, pt: "$1.5", children: "Package" }));
|
|
83
|
+
const next = async () => {
|
|
84
|
+
setPaging(true);
|
|
85
|
+
setNote('');
|
|
86
|
+
try {
|
|
87
|
+
const got = await shelf(t, { text, limit: PAGE, offset: list.length });
|
|
88
|
+
setMore((m) => [...m, ...got.listings]);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
setNote(e instanceof Error ? e.message : 'The next page did not load');
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
setPaging(false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return (_jsx(Part, { title: "The shelf", detail: "MCP servers the public registries publish. A plus adds one to your organization.", children: first.error && !page ? (_jsx(Soft, { children: first.error.message })) : !page ? (_jsx(Soft, { children: "Reading the shelf\u2026" })) : !list.length ? (_jsx(Soft, { children: text ? 'Nothing on the shelf matches.' : 'The shelf is empty.' })) : (_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} servers` : `${page.total} servers` }), _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] })) }));
|
|
98
|
+
}
|
|
99
|
+
/** The facts a card carries under its description. */
|
|
100
|
+
const facts = (l) => [l.vendor, l.official ? 'Official' : '', ready(l) ? 'Hosted' : 'Package only'].filter(Boolean).join(' · ');
|
|
101
|
+
/** `value` once it has held still for `ms`. */
|
|
102
|
+
function useSettled(value, ms) {
|
|
103
|
+
const [settled, setSettled] = useState(value);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const id = setTimeout(() => setSettled(value), ms);
|
|
106
|
+
return () => clearTimeout(id);
|
|
107
|
+
}, [value, ms]);
|
|
108
|
+
return settled;
|
|
109
|
+
}
|
|
110
|
+
/** The fleet's own servers, from the MCP server's tools/list. Every run can call them. */
|
|
111
|
+
function Natives({ q }) {
|
|
112
|
+
const t = useTarget();
|
|
113
|
+
const list = useRead(() => natives(t), [], [t]);
|
|
114
|
+
const [open, setOpen] = useState(null);
|
|
115
|
+
const shown = list.value.filter((s) => matches(q, s.name, s.description, ...s.ops));
|
|
116
|
+
const ops = shown.reduce((n, s) => n + s.ops.length, 0);
|
|
117
|
+
return (_jsx(Part, { title: "Built in", detail: "The fleet\u2019s own servers. A run starts one when it calls it, and nothing needs adding.", children: list.loading && !list.value.length ? (_jsx(Soft, { children: "Reading the servers\u2026" })) : list.error && !list.value.length ? (_jsx(Soft, { children: list.error.message })) : !shown.length ? (_jsx(Soft, { children: list.value.length ? 'No built-in server matches.' : 'The MCP server listed no native servers.' })) : (_jsxs(YStack, { gap: "$1", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " servers \u00B7 ", ops, " operations"] }), shown.map((s) => {
|
|
118
|
+
const on = open === s.name;
|
|
119
|
+
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 named no operations." })) })) : null] }, s.name));
|
|
120
|
+
})] })) }));
|
|
121
|
+
}
|
|
122
|
+
/** One listing in full: what it is, where it is served, and adding it. */
|
|
123
|
+
function About({ listing, added, signed, onAdd, onClose }) {
|
|
124
|
+
const t = useTarget();
|
|
125
|
+
const full = useRead(() => one(t, listing.id), listing, [t, listing.id]);
|
|
126
|
+
const l = full.value;
|
|
127
|
+
const links = [l.site, l.repo].filter((u) => u.startsWith('https://'));
|
|
128
|
+
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 somewhere 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: full.error.message }) : 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] }));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Add a server: a listing off the shelf, or any MCP URL. The secret, when it
|
|
132
|
+
* needs one, is sealed in KMS as it is added; then its tools are switched on.
|
|
133
|
+
*/
|
|
134
|
+
function Connect({ listing, onClose, onAdded }) {
|
|
135
|
+
const t = useTarget();
|
|
136
|
+
const [name, setName] = useState('');
|
|
137
|
+
const [url, setUrl] = useState('');
|
|
138
|
+
const [header, setHeader] = useState('Authorization');
|
|
139
|
+
const [secret, setSecret] = useState('');
|
|
140
|
+
const [working, setWorking] = useState(false);
|
|
141
|
+
const [note, setNote] = useState('');
|
|
142
|
+
const save = async () => {
|
|
143
|
+
setWorking(true);
|
|
144
|
+
setNote('');
|
|
145
|
+
const a = listing ? { listing: listing.id, name, header, secret } : { name, url, header, secret };
|
|
146
|
+
try {
|
|
147
|
+
const s = await add(t, a);
|
|
148
|
+
let said = `${s.name} is added.`;
|
|
149
|
+
try {
|
|
150
|
+
const its = (await tools(t, { source: 'mcp' })).filter((x) => owns(s, x.name)).map((x) => x.name);
|
|
151
|
+
if (its.length)
|
|
152
|
+
await toggle(t, its);
|
|
153
|
+
said = its.length ? `${s.name} is added, and its ${its.length} tools are on.` : `${s.name} is added. It listed no tools yet.`;
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
said = `${s.name} is added, but its tools could not be switched on: ${e instanceof Error ? e.message : 'no answer'}`;
|
|
157
|
+
}
|
|
158
|
+
onAdded(said);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
setNote(e instanceof Error ? e.message : 'The connector was not added');
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
setWorking(false);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return (_jsxs(Sheet, { title: listing ? `Add ${titleOf(listing)}` : 'Add connector', 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 join your organization\u2019s and 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. Sealed in KMS and sent in the header below on every call; 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" })] })] }));
|
|
168
|
+
}
|
|
169
|
+
/** One of the org's servers: where it is, its secret, its tools and their switches, and removing it. */
|
|
170
|
+
function Detail({ server, may, onClose, onDeleted }) {
|
|
171
|
+
const t = useTarget();
|
|
172
|
+
const list = useRead(() => tools(t, { source: 'mcp' }), [], [t, server.id]);
|
|
173
|
+
const its = useMemo(() => list.value.filter((x) => owns(server, x.name)), [list.value, server]);
|
|
174
|
+
const [busy, setBusy] = useState(false);
|
|
175
|
+
const [note, setNote] = useState('');
|
|
176
|
+
const [asking, setAsking] = useState(false);
|
|
177
|
+
const off = its.filter((x) => !x.activated).map((x) => x.name);
|
|
178
|
+
const flip = async (on, offNames) => {
|
|
179
|
+
setBusy(true);
|
|
180
|
+
setNote('');
|
|
181
|
+
try {
|
|
182
|
+
await toggle(t, on, offNames);
|
|
183
|
+
list.reload();
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
setBusy(false);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
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: "Asking the server for its tools\u2026" })) : list.error && !list.value.length ? (_jsx(Soft, { children: list.error.message })) : !its.length ? (_jsx(Soft, { children: "The server listed no tools. One that does not answer is left out until it does." })) : (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: its.map((x, i) => {
|
|
193
|
+
const short = x.name.slice(server.id.length + 1);
|
|
194
|
+
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));
|
|
195
|
+
}) })), _jsx(Line, { children: note || (may ? '' : 'An org admin adds, switches and removes the organization’s 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 () => {
|
|
196
|
+
await remove(t, server.id);
|
|
197
|
+
onDeleted();
|
|
198
|
+
} })] }));
|
|
199
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Customize: what the agent brings to a run — the skills it knows, the
|
|
4
|
+
* connectors it calls, the plugins it runs, and agents of the org's own. Each
|
|
5
|
+
* tab is its own address; in each, Yours is what the org has and Discover is
|
|
6
|
+
* what it can add, with one search box and one Add over both.
|
|
7
|
+
*
|
|
8
|
+
* Nothing here is gated to org admins, because the platform gates none of it:
|
|
9
|
+
* every write on these tabs is open to any member of the org.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { BookOpen, Bot, Plug, Plus, Puzzle } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { Button, Input } from '@hanzo/ui';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { useHost } from '../host.js';
|
|
16
|
+
import { path, TABS } from '../route.js';
|
|
17
|
+
import { Agents } from './agents.js';
|
|
18
|
+
import { Connectors } from './connectors.js';
|
|
19
|
+
import { Plugins } from './plugins.js';
|
|
20
|
+
import { Skills } from './skills.js';
|
|
21
|
+
import { Choice } from './ui.js';
|
|
22
|
+
/** What every run in the org loads: an org admin's to add to, a member's to read. */
|
|
23
|
+
const ORG_KIT = ['skills', 'connectors'];
|
|
24
|
+
const LABEL = {
|
|
25
|
+
skills: { title: 'Skills', add: 'New skill', find: 'Search skills', icon: _jsx(BookOpen, { size: 15 }) },
|
|
26
|
+
connectors: { title: 'Connectors', add: 'Add connector', find: 'Search connectors', icon: _jsx(Plug, { size: 15 }) },
|
|
27
|
+
plugins: { title: 'Plugins', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
|
|
28
|
+
agents: { title: 'Agents', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
|
|
29
|
+
};
|
|
30
|
+
const VIEWS = [
|
|
31
|
+
{ id: 'yours', label: 'Yours' },
|
|
32
|
+
{ id: 'discover', label: 'Discover' },
|
|
33
|
+
];
|
|
34
|
+
export function Customize({ tab, view: first }) {
|
|
35
|
+
const host = useHost();
|
|
36
|
+
// Until a person picks, a visitor sees what there is to add and a member what is theirs:
|
|
37
|
+
// sign-in lands after the first draw, so the default follows it rather than being fixed then.
|
|
38
|
+
const [picked, setView] = useState(first ?? null);
|
|
39
|
+
const view = picked ?? (host.person ? 'yours' : 'discover');
|
|
40
|
+
const [q, setQ] = useState('');
|
|
41
|
+
const [adding, setAdding] = useState(false);
|
|
42
|
+
const pick = (next) => {
|
|
43
|
+
setQ('');
|
|
44
|
+
setAdding(false);
|
|
45
|
+
host.go(path({ kind: 'customize', tab: next }));
|
|
46
|
+
};
|
|
47
|
+
const pane = { view, q, adding, onAdding: setAdding, onView: setView };
|
|
48
|
+
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 the agent brings to a run: the skills it knows, the connectors it calls, the plugins it runs, and agents of your own." })] }), _jsx(XStack, { role: "tablist", "aria-label": "Customize", gap: "$4", borderBottomWidth: 1, borderColor: "$borderColor", "$max-md": { gap: '$3' }, children: TABS.map((id) => {
|
|
49
|
+
const on = id === tab;
|
|
50
|
+
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));
|
|
51
|
+
}) }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Choice, { label: "Show", value: view, options: VIEWS, onChange: setView }), _jsx(XStack, { flex: 1 }), host.person && (host.admin || !ORG_KIT.includes(tab)) ? (_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 }), tab === 'skills' ? (_jsx(Skills, { ...pane })) : tab === 'connectors' ? (_jsx(Connectors, { ...pane })) : tab === 'plugins' ? (_jsx(Plugins, { ...pane })) : (_jsx(Agents, { ...pane }))] }) }));
|
|
52
|
+
}
|