@hanzo/build 0.2.10 → 0.2.11

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.
Files changed (136) hide show
  1. package/README.md +97 -25
  2. package/lib/api/agents.d.ts +80 -0
  3. package/lib/api/agents.js +148 -0
  4. package/lib/api/billing.d.ts +205 -0
  5. package/lib/api/billing.js +300 -0
  6. package/lib/api/call.d.ts +6 -0
  7. package/lib/api/call.js +11 -0
  8. package/lib/api/capabilities.d.ts +25 -0
  9. package/lib/api/capabilities.js +40 -0
  10. package/lib/api/coding.d.ts +31 -1
  11. package/lib/api/coding.js +50 -1
  12. package/lib/api/connectors.d.ts +90 -0
  13. package/lib/api/connectors.js +109 -0
  14. package/lib/api/consent.d.ts +22 -0
  15. package/lib/api/consent.js +28 -0
  16. package/lib/api/github.d.ts +13 -0
  17. package/lib/api/github.js +9 -0
  18. package/lib/api/harness.d.ts +44 -0
  19. package/lib/api/harness.js +348 -0
  20. package/lib/api/keys.d.ts +41 -0
  21. package/lib/api/keys.js +55 -0
  22. package/lib/api/machines.d.ts +56 -0
  23. package/lib/api/machines.js +84 -0
  24. package/lib/api/members.d.ts +47 -0
  25. package/lib/api/members.js +88 -0
  26. package/lib/api/memory.d.ts +25 -0
  27. package/lib/api/memory.js +34 -0
  28. package/lib/api/places.d.ts +2 -2
  29. package/lib/api/places.js +3 -26
  30. package/lib/api/plugins.d.ts +55 -0
  31. package/lib/api/plugins.js +62 -0
  32. package/lib/api/pref.d.ts +49 -0
  33. package/lib/api/pref.js +68 -0
  34. package/lib/api/profile.d.ts +21 -0
  35. package/lib/api/profile.js +40 -0
  36. package/lib/api/projects.d.ts +19 -2
  37. package/lib/api/projects.js +29 -4
  38. package/lib/api/provider.d.ts +42 -0
  39. package/lib/api/provider.js +58 -0
  40. package/lib/api/sessions.d.ts +32 -4
  41. package/lib/api/sessions.js +42 -6
  42. package/lib/api/skills.d.ts +70 -0
  43. package/lib/api/skills.js +90 -0
  44. package/lib/api/tools.d.ts +34 -0
  45. package/lib/api/tools.js +37 -0
  46. package/lib/api/turn.d.ts +84 -16
  47. package/lib/api/turn.js +221 -4
  48. package/lib/api/webhooks.d.ts +60 -0
  49. package/lib/api/webhooks.js +81 -0
  50. package/lib/ask.d.ts +18 -0
  51. package/lib/ask.js +69 -0
  52. package/lib/builder.js +39 -19
  53. package/lib/customize/agents.d.ts +2 -0
  54. package/lib/customize/agents.js +125 -0
  55. package/lib/customize/connectors.d.ts +2 -0
  56. package/lib/customize/connectors.js +197 -0
  57. package/lib/customize/index.d.ts +6 -0
  58. package/lib/customize/index.js +50 -0
  59. package/lib/customize/plugins.d.ts +2 -0
  60. package/lib/customize/plugins.js +93 -0
  61. package/lib/customize/skills.d.ts +2 -0
  62. package/lib/customize/skills.js +125 -0
  63. package/lib/customize/ui.d.ts +110 -0
  64. package/lib/customize/ui.js +99 -0
  65. package/lib/desk.d.ts +4 -1
  66. package/lib/desk.js +3 -16
  67. package/lib/find.d.ts +7 -0
  68. package/lib/find.js +77 -0
  69. package/lib/foot.d.ts +16 -0
  70. package/lib/foot.js +36 -0
  71. package/lib/forge.js +4 -4
  72. package/lib/git.d.ts +4 -0
  73. package/lib/git.js +6 -2
  74. package/lib/host.d.ts +5 -0
  75. package/lib/landing.d.ts +10 -0
  76. package/lib/landing.js +30 -14
  77. package/lib/markdown.d.ts +49 -0
  78. package/lib/markdown.js +123 -0
  79. package/lib/plans.d.ts +1 -0
  80. package/lib/plans.js +113 -0
  81. package/lib/prefs.d.ts +30 -0
  82. package/lib/prefs.js +83 -0
  83. package/lib/prose.d.ts +4 -0
  84. package/lib/prose.js +53 -0
  85. package/lib/route.d.ts +16 -1
  86. package/lib/route.js +32 -1
  87. package/lib/run.js +184 -32
  88. package/lib/section.js +2 -2
  89. package/lib/settings/account.d.ts +5 -0
  90. package/lib/settings/account.js +111 -0
  91. package/lib/settings/billing.d.ts +1 -0
  92. package/lib/settings/billing.js +104 -0
  93. package/lib/settings/capabilities.d.ts +1 -0
  94. package/lib/settings/capabilities.js +55 -0
  95. package/lib/settings/card.d.ts +31 -0
  96. package/lib/settings/card.js +105 -0
  97. package/lib/settings/code.d.ts +1 -0
  98. package/lib/settings/code.js +51 -0
  99. package/lib/settings/environments.d.ts +1 -0
  100. package/lib/settings/environments.js +53 -0
  101. package/lib/settings/general.d.ts +1 -0
  102. package/lib/settings/general.js +44 -0
  103. package/lib/settings/index.d.ts +4 -0
  104. package/lib/settings/index.js +22 -0
  105. package/lib/settings/integrations.d.ts +1 -0
  106. package/lib/settings/integrations.js +81 -0
  107. package/lib/settings/keys.d.ts +1 -0
  108. package/lib/settings/keys.js +102 -0
  109. package/lib/settings/machines.d.ts +1 -0
  110. package/lib/settings/machines.js +118 -0
  111. package/lib/settings/members.d.ts +1 -0
  112. package/lib/settings/members.js +67 -0
  113. package/lib/settings/memory.d.ts +1 -0
  114. package/lib/settings/memory.js +56 -0
  115. package/lib/settings/notifications.d.ts +1 -0
  116. package/lib/settings/notifications.js +97 -0
  117. package/lib/settings/privacy.d.ts +1 -0
  118. package/lib/settings/privacy.js +63 -0
  119. package/lib/settings/sections.d.ts +16 -0
  120. package/lib/settings/sections.js +32 -0
  121. package/lib/settings/ui.d.ts +49 -0
  122. package/lib/settings/ui.js +47 -0
  123. package/lib/settings/usage.d.ts +1 -0
  124. package/lib/settings/usage.js +114 -0
  125. package/lib/shelf.d.ts +6 -0
  126. package/lib/shelf.js +86 -16
  127. package/lib/switch.js +1 -2
  128. package/lib/transcript.d.ts +12 -0
  129. package/lib/transcript.js +56 -0
  130. package/lib/voice.d.ts +3 -1
  131. package/lib/voice.js +10 -4
  132. package/package.json +1 -1
  133. package/lib/account.d.ts +0 -14
  134. package/lib/account.js +0 -38
  135. package/lib/mcp.d.ts +0 -1
  136. 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
- * /-/mcp the fleet's native MCP servers
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 live status dot, and the
19
- * account. Its collapse is an explicit toggle kept per browser.
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, Puzzle, Workflow } from '@hanzogui/lucide-icons-2';
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 { useEffect, useMemo, useState } from 'react';
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 { Account, Find, onAccount } from './account.js';
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
- useEffect(() => onAccount(() => setAccount(true)), []);
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: () => host.go(''),
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('https://docs.hanzo.ai/docs/dev', '_blank', 'noopener,noreferrer') },
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
- ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
101
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => setAccount(true), 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 }))] }), _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(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
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,2 @@
1
+ import { type Pane } from './ui.tsx';
2
+ export declare function Agents({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;
@@ -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,2 @@
1
+ import { type Pane } from './ui.tsx';
2
+ export declare function Connectors({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;
@@ -0,0 +1,197 @@
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
+ const mine = useRead(signed ? () => servers(t) : null, [], [t, signed]);
32
+ const [opened, setOpened] = useState(null);
33
+ const [listing, setListing] = useState(null);
34
+ const [connecting, setConnecting] = useState(null);
35
+ const [note, setNote] = useState('');
36
+ const connect = adding || connecting ? (_jsx(Connect, { listing: connecting, onClose: () => {
37
+ setConnecting(null);
38
+ onAdding(false);
39
+ }, onAdded: (said) => {
40
+ setConnecting(null);
41
+ onAdding(false);
42
+ mine.reload();
43
+ onView('yours');
44
+ setNote(said);
45
+ } })) : null;
46
+ const detail = listing ? (_jsx(About, { listing: listing, added: mine.value.some((s) => s.listing === listing.id), signed: signed, onAdd: () => {
47
+ setConnecting(listing);
48
+ setListing(null);
49
+ }, onClose: () => setListing(null) })) : null;
50
+ if (view === 'discover') {
51
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Line, { children: note }), _jsx(Shelf, { q: q, signed: signed, added: new Set(mine.value.map((s) => s.listing).filter(Boolean)), onOpen: setListing, onAdd: setConnecting }), _jsx(Natives, { q: q }), detail, connect] }));
52
+ }
53
+ if (!signed) {
54
+ return _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your connectors." });
55
+ }
56
+ const shown = mine.value.filter((s) => matches(q, s.name, s.url, s.listing));
57
+ 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: "No connectors yet. Add an MCP server by its URL, or pick one off the shelf." })) : !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, onClose: () => setOpened(null), onDeleted: () => {
58
+ setNote(`${opened.name} is removed`);
59
+ setOpened(null);
60
+ mine.reload();
61
+ } })) : null, connect] }));
62
+ }
63
+ /** The shelf: featured first, then by name, a page at a time. The platform searches it. */
64
+ function Shelf({ q, signed, added, onOpen, onAdd }) {
65
+ const t = useTarget();
66
+ const host = useHost();
67
+ const text = useSettled(q.trim(), 300);
68
+ const first = useRead(signed ? () => shelf(t, { text, limit: PAGE }) : null, null, [t, signed, text]);
69
+ const [more, setMore] = useState([]);
70
+ const [paging, setPaging] = useState(false);
71
+ const [note, setNote] = useState('');
72
+ useEffect(() => setMore([]), [first.value]);
73
+ if (!signed) {
74
+ 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." }) }));
75
+ }
76
+ const page = first.value;
77
+ const list = [...(page?.listings ?? []), ...more];
78
+ const hero = !text ? list.find((l) => l.featured) : undefined;
79
+ const rest = hero ? list.filter((l) => l !== hero) : list;
80
+ const action = (l) => 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" }));
81
+ const next = async () => {
82
+ setPaging(true);
83
+ setNote('');
84
+ try {
85
+ const got = await shelf(t, { text, limit: PAGE, offset: list.length });
86
+ setMore((m) => [...m, ...got.listings]);
87
+ }
88
+ catch (e) {
89
+ setNote(e instanceof Error ? e.message : 'The next page did not load');
90
+ }
91
+ finally {
92
+ setPaging(false);
93
+ }
94
+ };
95
+ 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] })) }));
96
+ }
97
+ /** The facts a card carries under its description. */
98
+ const facts = (l) => [l.vendor, l.official ? 'Official' : '', ready(l) ? 'Hosted' : 'Package only'].filter(Boolean).join(' · ');
99
+ /** `value` once it has held still for `ms`. */
100
+ function useSettled(value, ms) {
101
+ const [settled, setSettled] = useState(value);
102
+ useEffect(() => {
103
+ const id = setTimeout(() => setSettled(value), ms);
104
+ return () => clearTimeout(id);
105
+ }, [value, ms]);
106
+ return settled;
107
+ }
108
+ /** The fleet's own servers, from the MCP server's tools/list. Every run can call them. */
109
+ function Natives({ q }) {
110
+ const t = useTarget();
111
+ const list = useRead(() => natives(t), [], [t]);
112
+ const [open, setOpen] = useState(null);
113
+ const shown = list.value.filter((s) => matches(q, s.name, s.description, ...s.ops));
114
+ const ops = shown.reduce((n, s) => n + s.ops.length, 0);
115
+ 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) => {
116
+ const on = open === s.name;
117
+ 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));
118
+ })] })) }));
119
+ }
120
+ /** One listing in full: what it is, where it is served, and adding it. */
121
+ function About({ listing, added, signed, onAdd, onClose }) {
122
+ const t = useTarget();
123
+ const full = useRead(() => one(t, listing.id), listing, [t, listing.id]);
124
+ const l = full.value;
125
+ const links = [l.site, l.repo].filter((u) => u.startsWith('https://'));
126
+ 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] }));
127
+ }
128
+ /**
129
+ * Add a server: a listing off the shelf, or any MCP URL. The secret, when it
130
+ * needs one, is sealed in KMS as it is added; then its tools are switched on.
131
+ */
132
+ function Connect({ listing, onClose, onAdded }) {
133
+ const t = useTarget();
134
+ const [name, setName] = useState('');
135
+ const [url, setUrl] = useState('');
136
+ const [header, setHeader] = useState('Authorization');
137
+ const [secret, setSecret] = useState('');
138
+ const [working, setWorking] = useState(false);
139
+ const [note, setNote] = useState('');
140
+ const save = async () => {
141
+ setWorking(true);
142
+ setNote('');
143
+ const a = listing ? { listing: listing.id, name, header, secret } : { name, url, header, secret };
144
+ try {
145
+ const s = await add(t, a);
146
+ let said = `${s.name} is added.`;
147
+ try {
148
+ const its = (await tools(t, { source: 'mcp' })).filter((x) => owns(s, x.name)).map((x) => x.name);
149
+ if (its.length)
150
+ await toggle(t, its);
151
+ said = its.length ? `${s.name} is added, and its ${its.length} tools are on.` : `${s.name} is added. It listed no tools yet.`;
152
+ }
153
+ catch (e) {
154
+ said = `${s.name} is added, but its tools could not be switched on: ${e instanceof Error ? e.message : 'no answer'}`;
155
+ }
156
+ onAdded(said);
157
+ }
158
+ catch (e) {
159
+ setNote(e instanceof Error ? e.message : 'The connector was not added');
160
+ }
161
+ finally {
162
+ setWorking(false);
163
+ }
164
+ };
165
+ 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" })] })] }));
166
+ }
167
+ /** One of the org's servers: where it is, its secret, its tools and their switches, and removing it. */
168
+ function Detail({ server, onClose, onDeleted }) {
169
+ const t = useTarget();
170
+ const list = useRead(() => tools(t, { source: 'mcp' }), [], [t, server.id]);
171
+ const its = useMemo(() => list.value.filter((x) => owns(server, x.name)), [list.value, server]);
172
+ const [busy, setBusy] = useState(false);
173
+ const [note, setNote] = useState('');
174
+ const [asking, setAsking] = useState(false);
175
+ const off = its.filter((x) => !x.activated).map((x) => x.name);
176
+ const flip = async (on, offNames) => {
177
+ setBusy(true);
178
+ setNote('');
179
+ try {
180
+ await toggle(t, on, offNames);
181
+ list.reload();
182
+ }
183
+ catch (e) {
184
+ setNote(e instanceof Error ? e.message : 'That did not work');
185
+ }
186
+ finally {
187
+ setBusy(false);
188
+ }
189
+ };
190
+ 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 ? (_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) => {
191
+ const short = x.name.slice(server.id.length + 1);
192
+ 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] }), _jsx(Switch, { checked: x.activated, disabled: busy, onCheckedChange: (v) => void flip(v ? [x.name] : [], v ? [] : [x.name]), "aria-label": `${short} on` })] }, x.name));
193
+ }) })), _jsx(Line, { children: note }), _jsx(XStack, { justify: "flex-start", children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Remove connector" }) }), _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 () => {
194
+ await remove(t, server.id);
195
+ onDeleted();
196
+ } })] }));
197
+ }
@@ -0,0 +1,6 @@
1
+ import { type Tab } from '../route.ts';
2
+ import { type View } from './ui.tsx';
3
+ export declare function Customize({ tab, view: first }: {
4
+ tab: Tab;
5
+ view?: View;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,50 @@
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
+ const LABEL = {
23
+ skills: { title: 'Skills', add: 'New skill', find: 'Search skills', icon: _jsx(BookOpen, { size: 15 }) },
24
+ connectors: { title: 'Connectors', add: 'Add connector', find: 'Search connectors', icon: _jsx(Plug, { size: 15 }) },
25
+ plugins: { title: 'Plugins', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
26
+ agents: { title: 'Agents', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
27
+ };
28
+ const VIEWS = [
29
+ { id: 'yours', label: 'Yours' },
30
+ { id: 'discover', label: 'Discover' },
31
+ ];
32
+ export function Customize({ tab, view: first }) {
33
+ const host = useHost();
34
+ // Until a person picks, a visitor sees what there is to add and a member what is theirs:
35
+ // sign-in lands after the first draw, so the default follows it rather than being fixed then.
36
+ const [picked, setView] = useState(first ?? null);
37
+ const view = picked ?? (host.person ? 'yours' : 'discover');
38
+ const [q, setQ] = useState('');
39
+ const [adding, setAdding] = useState(false);
40
+ const pick = (next) => {
41
+ setQ('');
42
+ setAdding(false);
43
+ host.go(path({ kind: 'customize', tab: next }));
44
+ };
45
+ const pane = { view, q, adding, onAdding: setAdding, onView: setView };
46
+ 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) => {
47
+ const on = id === tab;
48
+ 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));
49
+ }) }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Choice, { label: "Show", value: view, options: VIEWS, onChange: setView }), _jsx(XStack, { flex: 1 }), host.person ? (_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 }))] }) }));
50
+ }
@@ -0,0 +1,2 @@
1
+ import { type Pane } from './ui.tsx';
2
+ export declare function Plugins({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;