@hanzo/build 0.2.52 → 0.2.53

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