@hanzo/build 0.2.79 → 0.2.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +22 -4
  2. package/lib/activity.d.ts +3 -7
  3. package/lib/activity.js +18 -5
  4. package/lib/api/agents.d.ts +36 -35
  5. package/lib/api/agents.js +66 -29
  6. package/lib/api/auto.d.ts +3 -23
  7. package/lib/api/auto.js +39 -22
  8. package/lib/api/billing.d.ts +0 -35
  9. package/lib/api/billing.js +6 -5
  10. package/lib/api/call.d.ts +16 -8
  11. package/lib/api/call.js +32 -1
  12. package/lib/api/capabilities.d.ts +0 -11
  13. package/lib/api/capabilities.js +2 -1
  14. package/lib/api/codebases.d.ts +0 -10
  15. package/lib/api/codebases.js +3 -2
  16. package/lib/api/coding.d.ts +3 -32
  17. package/lib/api/coding.js +13 -7
  18. package/lib/api/connectors.d.ts +1 -17
  19. package/lib/api/connectors.js +5 -4
  20. package/lib/api/environment.d.ts +6 -20
  21. package/lib/api/environment.js +22 -19
  22. package/lib/api/github.d.ts +0 -22
  23. package/lib/api/github.js +3 -2
  24. package/lib/api/keys.d.ts +0 -14
  25. package/lib/api/keys.js +4 -3
  26. package/lib/api/library.d.ts +3 -17
  27. package/lib/api/library.js +23 -22
  28. package/lib/api/machines.d.ts +0 -15
  29. package/lib/api/machines.js +6 -5
  30. package/lib/api/mcp.d.ts +0 -9
  31. package/lib/api/mcp.js +3 -2
  32. package/lib/api/members.d.ts +0 -15
  33. package/lib/api/members.js +4 -3
  34. package/lib/api/memory.d.ts +0 -11
  35. package/lib/api/memory.js +2 -1
  36. package/lib/api/places.d.ts +1 -9
  37. package/lib/api/places.js +12 -1
  38. package/lib/api/plugins.d.ts +1 -16
  39. package/lib/api/plugins.js +4 -3
  40. package/lib/api/profile.d.ts +0 -10
  41. package/lib/api/profile.js +5 -4
  42. package/lib/api/projects.d.ts +0 -14
  43. package/lib/api/projects.js +2 -1
  44. package/lib/api/provider.d.ts +2 -14
  45. package/lib/api/provider.js +12 -10
  46. package/lib/api/sandbox.d.ts +0 -17
  47. package/lib/api/sandbox.js +5 -4
  48. package/lib/api/sessions.d.ts +0 -17
  49. package/lib/api/sessions.js +5 -4
  50. package/lib/api/skills.d.ts +1 -15
  51. package/lib/api/skills.js +5 -4
  52. package/lib/api/studio.d.ts +0 -14
  53. package/lib/api/studio.js +6 -5
  54. package/lib/api/turn.d.ts +6 -28
  55. package/lib/api/turn.js +105 -49
  56. package/lib/api/verdict.d.ts +0 -9
  57. package/lib/api/verdict.js +2 -1
  58. package/lib/api/webhooks.d.ts +1 -14
  59. package/lib/api/webhooks.js +5 -4
  60. package/lib/ask.js +7 -4
  61. package/lib/automations.d.ts +5 -4
  62. package/lib/automations.js +95 -74
  63. package/lib/back.js +10 -4
  64. package/lib/builder.js +7 -5
  65. package/lib/carry.js +20 -16
  66. package/lib/customize/agents.d.ts +3 -0
  67. package/lib/customize/agents.js +152 -55
  68. package/lib/customize/connectors.js +62 -48
  69. package/lib/customize/index.js +45 -14
  70. package/lib/customize/plugins.js +34 -22
  71. package/lib/customize/say.d.ts +13 -1
  72. package/lib/customize/say.js +15 -21
  73. package/lib/customize/skills.js +31 -22
  74. package/lib/customize/ui.d.ts +5 -9
  75. package/lib/customize/ui.js +26 -16
  76. package/lib/data.d.ts +1 -1
  77. package/lib/data.js +3 -2
  78. package/lib/desk.js +45 -33
  79. package/lib/door.js +35 -10
  80. package/lib/environment.js +25 -19
  81. package/lib/files.js +43 -34
  82. package/lib/find.js +12 -9
  83. package/lib/foot.js +7 -4
  84. package/lib/forge.js +61 -40
  85. package/lib/gear.d.ts +2 -1
  86. package/lib/gear.js +6 -4
  87. package/lib/git.js +29 -19
  88. package/lib/host.d.ts +3 -1
  89. package/lib/landing.d.ts +5 -5
  90. package/lib/landing.js +52 -44
  91. package/lib/library.js +73 -51
  92. package/lib/meter.d.ts +1 -1
  93. package/lib/meter.js +14 -5
  94. package/lib/mic.js +12 -5
  95. package/lib/mind.d.ts +1 -0
  96. package/lib/mind.js +5 -3
  97. package/lib/nav.d.ts +6 -5
  98. package/lib/nav.js +37 -30
  99. package/lib/panel.d.ts +2 -1
  100. package/lib/panel.js +52 -36
  101. package/lib/plan.d.ts +4 -20
  102. package/lib/plan.js +26 -10
  103. package/lib/plans.js +49 -30
  104. package/lib/plugins/design.js +3 -2
  105. package/lib/plugins/index.d.ts +23 -4
  106. package/lib/plugins/index.js +11 -26
  107. package/lib/plugins/pdf.js +2 -1
  108. package/lib/plugins/project.js +6 -3
  109. package/lib/plugins/research.js +3 -2
  110. package/lib/plugins/sketch.js +6 -3
  111. package/lib/plugins/slides.js +3 -2
  112. package/lib/plugins/template.js +6 -3
  113. package/lib/plus.js +25 -13
  114. package/lib/prefs.d.ts +0 -14
  115. package/lib/prefs.js +2 -1
  116. package/lib/project.js +69 -62
  117. package/lib/prompt.d.ts +12 -11
  118. package/lib/prompt.js +38 -20
  119. package/lib/publish.js +17 -12
  120. package/lib/reply.js +6 -3
  121. package/lib/run.js +72 -54
  122. package/lib/say.d.ts +2 -0
  123. package/lib/say.js +7 -0
  124. package/lib/scheduled.js +54 -43
  125. package/lib/section.d.ts +1 -0
  126. package/lib/section.js +8 -2
  127. package/lib/settings/account.d.ts +1 -0
  128. package/lib/settings/account.js +31 -21
  129. package/lib/settings/billing.js +55 -28
  130. package/lib/settings/capabilities.js +26 -15
  131. package/lib/settings/card.js +19 -12
  132. package/lib/settings/code.js +22 -11
  133. package/lib/settings/credits.d.ts +4 -4
  134. package/lib/settings/credits.js +34 -21
  135. package/lib/settings/environments.js +21 -14
  136. package/lib/settings/general.js +26 -14
  137. package/lib/settings/index.js +6 -3
  138. package/lib/settings/integrations.js +42 -28
  139. package/lib/settings/keys.js +37 -19
  140. package/lib/settings/list.d.ts +0 -5
  141. package/lib/settings/list.js +24 -15
  142. package/lib/settings/machines.d.ts +3 -0
  143. package/lib/settings/machines.js +63 -38
  144. package/lib/settings/members.js +23 -14
  145. package/lib/settings/memory.js +12 -6
  146. package/lib/settings/notifications.js +32 -19
  147. package/lib/settings/plan.d.ts +1 -1
  148. package/lib/settings/plan.js +40 -17
  149. package/lib/settings/privacy.js +26 -13
  150. package/lib/settings/ui.d.ts +3 -2
  151. package/lib/settings/ui.js +9 -3
  152. package/lib/settings/usage.js +7 -3
  153. package/lib/shelf.js +18 -15
  154. package/lib/side.d.ts +1 -26
  155. package/lib/side.js +11 -5
  156. package/lib/standing.d.ts +2 -10
  157. package/lib/standing.js +5 -3
  158. package/lib/suggest.d.ts +8 -28
  159. package/lib/suggest.js +39 -4
  160. package/lib/suggestions.js +14 -5
  161. package/lib/summary.d.ts +1 -1
  162. package/lib/summary.js +4 -1
  163. package/lib/sync.js +22 -12
  164. package/lib/tools.d.ts +4 -3
  165. package/lib/tools.js +10 -7
  166. package/lib/transcript.js +10 -8
  167. package/package.json +4 -2
@@ -11,8 +11,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * all its tools on its card and one per tool inside; a tool is called only
12
12
  * while it is on. Adding a server switches its tools on. A server's secret is
13
13
  * sealed in KMS and never shown again.
14
+ *
15
+ * Its words are the reader's language (@hanzo/i18n `t`); a server's name,
16
+ * address, description and tools are the registry's or the org's, drawn as given.
14
17
  */
15
18
  import { SizableText, XStack, YStack } from '@hanzo/gui';
19
+ import { useI18n } from '@hanzo/i18n/react';
16
20
  import { ChevronDown, ExternalLink, KeyRound, Server as ServerIcon } from '@hanzogui/lucide-icons-2';
17
21
  import { Button, Input, Switch } from '@hanzo/ui';
18
22
  import { useEffect, useMemo, useState } from 'react';
@@ -24,19 +28,20 @@ import { Out } from '../out.js';
24
28
  import { route } from '../route.js';
25
29
  import { useLoad } from './load.js';
26
30
  import { say } from './say.js';
27
- import { Add, Confirm, Empty, Failed, Featured, Field, Grid, left, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
31
+ import { Add, Confirm, Empty, Failed, Featured, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
28
32
  const PAGE = 48;
29
33
  /** The part of a URL a person reads: its host and path. */
30
34
  const where = (url) => url.replace(/^https?:\/\//, '').replace(/\/$/, '');
31
35
  export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
36
+ const { t } = useI18n();
32
37
  const host = useHost();
33
- const t = useTarget();
38
+ const target = useTarget();
34
39
  const signed = Boolean(host.person);
35
40
  // What every run in the org connects to is an org admin's to change; a member reads it.
36
41
  const may = signed && host.admin;
37
- const mine = useLoad(signed ? () => servers(t) : null, [], [t, signed]);
42
+ const mine = useLoad(signed ? () => servers(target) : null, [], [target, signed]);
38
43
  // Every server's tools, for the switch on each card; read once for all of them.
39
- const plane = useLoad(signed && view === 'yours' && mine.value.length ? () => tools(t, { source: 'mcp' }) : null, [], [t, signed, view, mine.value]);
44
+ const plane = useLoad(signed && view === 'yours' && mine.value.length ? () => tools(target, { source: 'mcp' }) : null, [], [target, signed, view, mine.value]);
40
45
  const [opened, setOpened] = useState(null);
41
46
  const [listing, setListing] = useState(null);
42
47
  const [connecting, setConnecting] = useState(null);
@@ -51,12 +56,12 @@ export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
51
56
  setBusy(s.id);
52
57
  setNote('');
53
58
  try {
54
- await toggle(t, next ? its : [], next ? [] : its);
59
+ await toggle(target, next ? its : [], next ? [] : its);
55
60
  plane.reload();
56
- setNote(next ? `${s.name} is on` : `${s.name} is off`);
61
+ setNote(next ? t('{name} is on', { name: s.name }) : t('{name} is off', { name: s.name }));
57
62
  }
58
63
  catch (e) {
59
- setNote(say(e));
64
+ setNote(say(e, t));
60
65
  }
61
66
  finally {
62
67
  setBusy('');
@@ -80,40 +85,41 @@ export function Connectors({ view, q, adding, onAdding, onView, onCount }) {
80
85
  return (_jsxs(YStack, { gap: "$6", children: [_jsx(Line, { children: note }), _jsx(Shelf, { q: q, signed: signed, may: may, added: new Set(mine.value.map((s) => s.listing).filter(Boolean)), onOpen: setListing, onAdd: setConnecting }), _jsx(Natives, { q: q }), detail, connect] }));
81
86
  }
82
87
  if (!signed) {
83
- return _jsx(Visitor, { children: "Sign in to see your connectors." });
88
+ return _jsx(Visitor, { children: t('Sign in to see your connectors.') });
84
89
  }
85
90
  const shown = mine.value.filter((s) => matches(q, s.name, s.url, s.listing));
86
- return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "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) => {
91
+ return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: t('Loading your connectors…') })) : !mine.value.length ? (_jsxs(Empty, { title: t('No connectors yet'), detail: may ? t('Connect an app from Browse, or add any MCP server by its URL.') : t('An org admin adds connectors. Browse what there is.'), children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: t('Browse connectors') }), may ? (_jsx(Button, { size: "sm", onPress: () => onAdding(true), children: t('Add by URL') })) : null] })) : !shown.length ? (_jsx(Soft, { children: t('No connector of yours matches “{q}”.', { q: q.trim() }) })) : (_jsx(Grid, { label: t('Your connectors'), children: shown.map((s) => {
87
92
  const its = plane.value.filter((x) => owns(s, x.name));
88
93
  const lit = its.filter((x) => x.activated).length;
89
94
  return (_jsx(Tile, { title: s.name, detail: where(s.url), meta: [
90
- its.length ? `${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',
95
+ its.length ? t('{on} of {n} tools on', { on: lit, n: its.length }) : plane.loading ? '' : t('No tools listed yet'),
96
+ s.listing ? t('From Browse') : t('Added by URL'),
97
+ s.secret ? t('Secret in KMS') : '',
98
+ s.admitted ? '' : t('Not in runs until an admin adds it again'),
94
99
  ]
95
100
  .filter(Boolean)
96
- .join(' · '), mark: _jsx(Mark, { name: s.name }), onOpen: () => setOpened(s), action: may && its.length ? (_jsx(Switch, { checked: lit > 0, disabled: busy === s.id, onCheckedChange: (v) => void flip(s, v), "aria-label": `${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: () => {
101
+ .join(' · '), mark: _jsx(Mark, { name: s.name }), onOpen: () => setOpened(s), action: may && its.length ? (_jsx(Switch, { checked: lit > 0, disabled: busy === s.id, onCheckedChange: (v) => void flip(s, v), "aria-label": t('{name} on', { name: s.name }) })) : its.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: lit ? t('On') : t('Off') })) : undefined }, s.id));
102
+ }) })), plane.error && mine.value.length ? _jsx(Line, { children: t('Tools: {reason}', { reason: say(plane.error, t) }) }) : null, opened ? (_jsx(Detail, { server: opened, may: may, onClose: () => {
98
103
  setOpened(null);
99
104
  plane.reload();
100
105
  }, onDeleted: () => {
101
- setNote(`${opened.name} is removed`);
106
+ setNote(t('{name} is removed', { name: opened.name }));
102
107
  setOpened(null);
103
108
  mine.reload();
104
109
  } })) : null, connect] }));
105
110
  }
106
111
  /** The shelf: featured first, then by name, a page at a time. The platform searches it. */
107
112
  function Shelf({ q, signed, may, added, onOpen, onAdd }) {
108
- const t = useTarget();
113
+ const { t } = useI18n();
114
+ const target = useTarget();
109
115
  const text = useSettled(q.trim(), 300);
110
- const first = useLoad(signed ? () => shelf(t, { text, limit: PAGE }) : null, null, [t, signed, text]);
116
+ const first = useLoad(signed ? () => shelf(target, { text, limit: PAGE }) : null, null, [target, signed, text]);
111
117
  const [more, setMore] = useState([]);
112
118
  const [paging, setPaging] = useState(false);
113
119
  const [note, setNote] = useState('');
114
120
  useEffect(() => setMore([]), [first.value]);
115
121
  if (!signed) {
116
- return (_jsx(Part, { title: "Connect an app", detail: "Give runs the tools you already use.", children: _jsx(Visitor, { children: "Sign in to browse connectors." }) }));
122
+ return (_jsx(Part, { title: t('Connect an app'), detail: t('Give runs the tools you already use.'), children: _jsx(Visitor, { children: t('Sign in to browse connectors.') }) }));
117
123
  }
118
124
  const page = first.value;
119
125
  const list = [...(page?.listings ?? []), ...more];
@@ -125,20 +131,20 @@ function Shelf({ q, signed, may, added, onOpen, onAdd }) {
125
131
  setPaging(true);
126
132
  setNote('');
127
133
  try {
128
- const got = await shelf(t, { text, limit: PAGE, offset: list.length });
134
+ const got = await shelf(target, { text, limit: PAGE, offset: list.length });
129
135
  setMore((m) => [...m, ...got.listings]);
130
136
  }
131
137
  catch (e) {
132
- setNote(say(e));
138
+ setNote(say(e, t));
133
139
  }
134
140
  finally {
135
141
  setPaging(false);
136
142
  }
137
143
  };
138
- return (_jsx(Part, { title: "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] })) }));
144
+ return (_jsx(Part, { title: t('Connect an app'), detail: t('Give runs the tools you already use. Adding one turns its tools on for your organization.'), children: first.error && !page ? (_jsx(Failed, { error: first.error, onRetry: first.reload })) : !page ? (_jsx(Soft, { children: t('Loading connectors…') })) : !list.length ? (_jsx(Soft, { children: text ? t('No connector matches “{q}”.', { q: text }) : t('There are no connectors to add yet.') })) : (_jsxs(YStack, { gap: "$3", children: [hero ? (_jsx(Featured, { title: titleOf(hero), detail: hero.description, meta: facts(hero, t), mark: _jsx(Mark, { name: titleOf(hero), logo: hero.logo, size: 48 }), onOpen: () => onOpen(hero), action: action(hero) })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: list.length < page.total ? t('{shown} of {n} connectors', { shown: list.length, n: page.total }) : t('{n} connectors', { n: page.total }) }), _jsx(Grid, { label: t('Servers on the shelf'), children: rest.map((l) => (_jsx(Tile, { title: titleOf(l), detail: l.description, meta: facts(l, t), mark: _jsx(Mark, { name: titleOf(l), logo: l.logo }), onOpen: () => onOpen(l), action: action(l) }, l.id))) }), _jsx(Line, { children: note }), list.length < page.total ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", variant: "outline", disabled: paging, onPress: () => void next(), children: t('Show more') }) })) : null] })) }));
139
145
  }
140
- /** The facts a card carries under its description. */
141
- const facts = (l) => [l.vendor, l.official ? 'Official' : '', ready(l) ? 'Hosted' : 'Package only: needs a place to run'].filter(Boolean).join(' · ');
146
+ /** The facts a card carries under its description, said in the reader's language. */
147
+ const facts = (l, t) => [l.vendor, l.official ? t('Official') : '', ready(l) ? t('Hosted') : t('Package only: needs a place to run')].filter(Boolean).join(' · ');
142
148
  /** `value` once it has held still for `ms`. */
143
149
  function useSettled(value, ms) {
144
150
  const [settled, setSettled] = useState(value);
@@ -154,33 +160,36 @@ function useSettled(value, ms) {
154
160
  * at once on the old MCP address, which exists to show them.
155
161
  */
156
162
  function Natives({ q }) {
163
+ const { t } = useI18n();
157
164
  const host = useHost();
158
- const t = useTarget();
165
+ const target = useTarget();
159
166
  const r = route(host.path);
160
167
  const [asked, setAsked] = useState(r.kind === 'screen' && r.screen === 'mcp');
161
- const list = useLoad(asked ? () => natives(t) : null, [], [t, asked]);
168
+ const list = useLoad(asked ? () => natives(target) : null, [], [target, asked]);
162
169
  const [open, setOpen] = useState(null);
163
170
  const shown = list.value.filter((s) => matches(q, s.name, s.description, ...s.ops));
164
171
  const ops = shown.reduce((n, s) => n + s.ops.length, 0);
165
- return (_jsx(Part, { title: "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) => {
172
+ return (_jsx(Part, { title: t('Built in'), detail: t('Hanzo’s own servers. Always on: every run can call them, and nothing needs adding.'), action: asked ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAsked(true), children: t('Show built-in servers') })), children: !asked ? null : list.loading && !list.value.length ? (_jsx(Soft, { children: t('Loading built-in servers…') })) : list.error && !list.value.length ? (_jsx(Failed, { error: list.error, onRetry: list.reload })) : !shown.length ? (_jsx(Soft, { children: list.value.length ? t('No built-in server matches “{q}”.', { q: q.trim() }) : t('No built-in servers are listed.') })) : (_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('{servers} servers · {ops} operations', { servers: shown.length, ops }) }), shown.map((s) => {
166
173
  const on = open === s.name;
167
- return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, 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));
174
+ return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, "$platform-web": { textAlign: 'start' }, onPress: () => setOpen(on ? null : s.name), children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: s.name }), s.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: on ? undefined : 2, children: s.description })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: s.ops.length }), _jsx(ChevronDown, { size: 14, style: { transform: on ? 'rotate(180deg)' : undefined } })] }), on ? (_jsx(YStack, { px: "$3", py: "$2", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: s.ops.length ? (s.ops.map((op) => (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: op }, op)))) : (_jsx(SizableText, { size: "$1", color: "$soft", children: t('This server lists no operations.') })) })) : null] }, s.name));
168
175
  })] })) }));
169
176
  }
170
177
  /** One listing in full: what it is, where it is served, and adding it. */
171
178
  function About({ listing, added, signed, onAdd, onClose }) {
172
- const t = useTarget();
173
- const full = useLoad(() => one(t, listing.id), listing, [t, listing.id]);
179
+ const { t } = useI18n();
180
+ const target = useTarget();
181
+ const full = useLoad(() => one(target, listing.id), listing, [target, listing.id]);
174
182
  const l = full.value;
175
183
  const links = [l.site, l.repo].filter((u) => u.startsWith('https://'));
176
- return (_jsxs(Sheet, { title: titleOf(l), open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(Mark, { name: titleOf(l), logo: l.logo, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: mono, children: l.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [l.vendor, l.version ? `v${l.version}` : '', l.official ? 'Official' : ''].filter(Boolean).join(' · ') })] })] }), l.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: l.description })) : null, l.remotes.length ? (_jsx(Field, { label: "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] }));
184
+ return (_jsxs(Sheet, { title: titleOf(l), open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(Mark, { name: titleOf(l), logo: l.logo, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: mono, children: l.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [l.vendor, l.version ? `v${l.version}` : '', l.official ? t('Official') : ''].filter(Boolean).join(' · ') })] })] }), l.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: l.description })) : null, l.remotes.length ? (_jsx(Field, { label: t('Served at'), children: l.remotes.map((r) => (_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 1, children: [r.url, " \u00B7 ", r.transport] }, r.url))) })) : null, l.packages.length ? (_jsx(Field, { label: t('Packages'), hint: ready(l) ? undefined : t('It ships only as a package, so it needs a place to run before it can be added here.'), children: l.packages.map((p) => (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 1, children: [p.registry, p.identifier, p.version, p.runtime].filter(Boolean).join(' · ') }, `${p.registry}:${p.identifier}`))) })) : null, links.length ? (_jsx(XStack, { gap: "$4", flexWrap: "wrap", children: links.map((u) => (_jsxs(Out, { href: u, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: where(u) }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }, u))) })) : null, full.error ? _jsx(Line, { children: t('Showing what Browse listed. {reason}', { reason: say(full.error, t) }) }) : null, signed && ready(l) ? (_jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", disabled: added, onPress: onAdd, children: added ? t('Added') : t('Add to your connectors') }) })) : null] }));
177
185
  }
178
186
  /**
179
187
  * Add a server: a listing off the shelf, or any MCP URL. The secret, when it
180
188
  * needs one, is sealed in KMS as it is added; then its tools are switched on.
181
189
  */
182
190
  function Connect({ listing, onClose, onAdded }) {
183
- const t = useTarget();
191
+ const { t } = useI18n();
192
+ const target = useTarget();
184
193
  const [name, setName] = useState('');
185
194
  const [url, setUrl] = useState('');
186
195
  const [header, setHeader] = useState('Authorization');
@@ -192,32 +201,33 @@ function Connect({ listing, onClose, onAdded }) {
192
201
  setNote('');
193
202
  const a = listing ? { listing: listing.id, name, header, secret } : { name, url, header, secret };
194
203
  try {
195
- const s = await add(t, a);
196
- let said = `${s.name} is added.`;
204
+ const s = await add(target, a);
205
+ let said = t('{name} is added.', { name: s.name });
197
206
  try {
198
- const its = (await tools(t, { source: 'mcp' })).filter((x) => owns(s, x.name)).map((x) => x.name);
207
+ const its = (await tools(target, { source: 'mcp' })).filter((x) => owns(s, x.name)).map((x) => x.name);
199
208
  if (its.length)
200
- await toggle(t, its);
201
- said = its.length ? `${s.name} is added, and its ${its.length} tools are on.` : `${s.name} is added. It listed no tools yet.`;
209
+ await toggle(target, its);
210
+ said = its.length ? t('{name} is added, and its {n} tools are on.', { name: s.name, n: its.length }) : t('{name} is added. It listed no tools yet.', { name: s.name });
202
211
  }
203
212
  catch (e) {
204
- said = `${s.name} is added, but its tools could not be switched on. ${say(e)}`;
213
+ said = t('{name} is added, but its tools could not be switched on. {reason}', { name: s.name, reason: say(e, t) });
205
214
  }
206
215
  onAdded(said);
207
216
  }
208
217
  catch (e) {
209
- setNote(say(e));
218
+ setNote(say(e, t));
210
219
  }
211
220
  finally {
212
221
  setWorking(false);
213
222
  }
214
223
  };
215
- return (_jsxs(Sheet, { title: listing ? `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" })] })] }));
224
+ return (_jsxs(Sheet, { title: listing ? t('Add {name}', { name: titleOf(listing) }) : t('Add by URL'), open: true, onOpenChange: (o) => !o && onClose(), width: 500, children: [listing ? (_jsx(SizableText, { size: "$2", color: "$soft", children: listing.description || listing.name })) : (_jsx(SizableText, { size: "$2", color: "$soft", children: t('Any MCP server that speaks HTTP. Its tools are on once it is added.') })), _jsx(Field, { label: t('Name'), hint: listing ? t('Leave it empty to use the listing’s own.') : undefined, children: _jsx(Input, { value: name, onChangeText: setName, placeholder: listing ? titleOf(listing) : t('Docs'), "aria-label": t('Name') }) }), listing ? null : (_jsx(Field, { label: "URL", hint: t('A public http(s) address. Private and loopback addresses are refused.'), children: _jsx(Input, { value: url, onChangeText: setUrl, placeholder: "https://mcp.example.com/mcp", "aria-label": "URL", autoCapitalize: "none" }) })), _jsx(Field, { label: t('Secret'), hint: t('Optional. Kept sealed in KMS, sent on every call, and never shown again.'), children: _jsx(Input, { value: secret, onChangeText: setSecret, placeholder: "Bearer \u2026", "aria-label": t('Secret'), secureTextEntry: true }) }), secret ? (_jsx(Field, { label: t('Header'), children: _jsx(Input, { value: header, onChangeText: setHeader, "aria-label": t('Header'), autoCapitalize: "none" }) })) : null, _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: t('Add') })] })] }));
216
225
  }
217
226
  /** One of the org's servers: where it is, its secret, its tools and their switches, and removing it. */
218
227
  function Detail({ server, may, onClose, onDeleted }) {
219
- const t = useTarget();
220
- const list = useLoad(() => tools(t, { source: 'mcp' }), [], [t, server.id]);
228
+ const { t } = useI18n();
229
+ const target = useTarget();
230
+ const list = useLoad(() => tools(target, { source: 'mcp' }), [], [target, server.id]);
221
231
  const its = useMemo(() => list.value.filter((x) => owns(server, x.name)), [list.value, server]);
222
232
  const [busy, setBusy] = useState(false);
223
233
  const [note, setNote] = useState('');
@@ -227,21 +237,25 @@ function Detail({ server, may, onClose, onDeleted }) {
227
237
  setBusy(true);
228
238
  setNote('');
229
239
  try {
230
- await toggle(t, on, offNames);
240
+ await toggle(target, on, offNames);
231
241
  list.reload();
232
242
  }
233
243
  catch (e) {
234
- setNote(say(e));
244
+ setNote(say(e, t));
235
245
  }
236
246
  finally {
237
247
  setBusy(false);
238
248
  }
239
249
  };
240
- return (_jsxs(Sheet, { title: server.name, open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 2, children: server.url }), _jsxs(XStack, { items: "center", gap: "$1.5", children: [server.secret ? _jsx(KeyRound, { size: 12, opacity: 0.7 }) : _jsx(ServerIcon, { size: 12, opacity: 0.7 }), _jsx(SizableText, { size: "$1", color: "$soft", children: server.secret ? `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) => {
250
+ return (_jsxs(Sheet, { title: server.name, open: true, onOpenChange: (o) => !o && onClose(), children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, numberOfLines: 2, children: server.url }), _jsxs(XStack, { items: "center", gap: "$1.5", children: [server.secret ? _jsx(KeyRound, { size: 12, opacity: 0.7 }) : _jsx(ServerIcon, { size: 12, opacity: 0.7 }), _jsx(SizableText, { size: "$1", color: "$soft", children: server.secret
251
+ ? server.header
252
+ ? t('A secret is sealed in KMS and sent in {header}.', { header: server.header })
253
+ : t('A secret is sealed in KMS and sent in its header.')
254
+ : t('No secret: it is called as it is.') })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", flex: 1, children: t('Tools') }), its.length && may ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => void (off.length ? flip(off, []) : flip([], its.map((x) => x.name))), children: off.length ? t('Turn all on') : t('Turn all off') })) : null] }), list.loading && !list.value.length ? (_jsx(Soft, { children: t('Loading its tools…') })) : list.error && !list.value.length ? (_jsx(Failed, { error: list.error, onRetry: list.reload })) : !its.length ? (_jsx(Soft, { children: t('No tools listed yet. A server that does not answer shows its tools once it does.') })) : (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: its.map((x, i) => {
241
255
  const short = x.name.slice(server.id.length + 1);
242
- return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: short }), x.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })) : null] }), may ? (_jsx(Switch, { checked: x.activated, disabled: busy, onCheckedChange: (v) => void flip(v ? [x.name] : [], v ? [] : [x.name]), "aria-label": `${short} on` })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.activated ? 'On' : 'Off' }))] }, x.name));
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 () => {
244
- await remove(t, server.id);
256
+ return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: short }), x.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })) : null] }), may ? (_jsx(Switch, { checked: x.activated, disabled: busy, onCheckedChange: (v) => void flip(v ? [x.name] : [], v ? [] : [x.name]), "aria-label": t('{name} on', { name: short }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.activated ? t('On') : t('Off') }))] }, x.name));
257
+ }) })), _jsx(Line, { children: note || (may ? '' : t('Only an org admin can switch or remove connectors.')) }), may ? (_jsx(XStack, { justify: "flex-start", children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: t('Remove connector') }) })) : null, _jsx(Confirm, { what: server.name, says: t('Its tools leave your organization and every agent, and its sealed secret is destroyed.'), open: asking, onOpenChange: setAsking, onYes: async () => {
258
+ await remove(target, server.id);
245
259
  onDeleted();
246
260
  } })] }));
247
261
  }
@@ -9,8 +9,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  * Skills and connectors ride into every run in the org, so adding, switching
10
10
  * and removing them is an org admin's and a member reads them; plugins and
11
11
  * agents are any member's.
12
+ *
13
+ * Its words are the reader's language (@hanzo/i18n `t`).
12
14
  */
13
15
  import { SizableText, XStack, YStack } from '@hanzo/gui';
16
+ import { phrase } from '@hanzo/i18n';
17
+ import { useI18n } from '@hanzo/i18n/react';
14
18
  import { BookOpen, Bot, Plug, Plus, Puzzle } from '@hanzogui/lucide-icons-2';
15
19
  import { Button, Input } from '@hanzo/ui';
16
20
  import { useCallback, useState } from 'react';
@@ -23,17 +27,44 @@ import { Skills } from './skills.js';
23
27
  import { Choice } from './ui.js';
24
28
  /** What every run in the org loads: an org admin's to add to, a member's to read. */
25
29
  export const ORG_KIT = ['skills', 'connectors'];
30
+ /**
31
+ * Each tab's words, English and marked (`phrase`), drawn through `t`: its name,
32
+ * its line, its add control, its search and the search's hint, and, for a tab
33
+ * an org admin keeps (ORG_KIT), what a member is told.
34
+ */
26
35
  const LABEL = {
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 }) },
36
+ skills: {
37
+ title: phrase('Skills'),
38
+ says: phrase('Know-how an agent reads before it works.'),
39
+ add: phrase('New skill'),
40
+ find: phrase('Search skills'),
41
+ typing: phrase('Search skills…'),
42
+ admin: phrase('Only an org admin can add or change skills. You can see what is on.'),
43
+ icon: _jsx(BookOpen, { size: 15 }),
44
+ },
45
+ connectors: {
46
+ title: phrase('Connectors'),
47
+ says: phrase('Apps and tools an agent can call.'),
48
+ add: phrase('Add by URL'),
49
+ find: phrase('Search connectors'),
50
+ typing: phrase('Search connectors…'),
51
+ admin: phrase('Only an org admin can add or change connectors. You can see what is on.'),
52
+ icon: _jsx(Plug, { size: 15 }),
53
+ },
54
+ plugins: { title: phrase('Plugins'), says: phrase('Connectors you build in TypeScript.'), add: phrase('Build plugin'), find: phrase('Search plugins'), typing: phrase('Search plugins…'), icon: _jsx(Puzzle, { size: 15 }) },
55
+ agents: {
56
+ title: phrase('Agents'),
57
+ says: phrase('Agents with their own model, instructions and budget.'),
58
+ add: phrase('New agent'),
59
+ find: phrase('Search agents'),
60
+ typing: phrase('Search agents…'),
61
+ icon: _jsx(Bot, { size: 15 }),
62
+ },
31
63
  };
32
- /** Where a tab opens: an org's own agents (and the cast to add) for Agents; Browse for the rest, what there is to add being the page. */
33
- const opens = (tab) => (tab === 'agents' ? 'yours' : 'discover');
34
64
  export function Customize({ tab, view: first }) {
65
+ const { t } = useI18n();
35
66
  const host = useHost();
36
- const [view, setView] = useState(first ?? opens(tab));
67
+ const [view, setView] = useState(first ?? 'discover');
37
68
  const [q, setQ] = useState('');
38
69
  const [adding, setAdding] = useState(false);
39
70
  const [count, setCount] = useState(0);
@@ -41,18 +72,18 @@ export function Customize({ tab, view: first }) {
41
72
  const pick = (next) => {
42
73
  setQ('');
43
74
  setAdding(false);
44
- setView(opens(next));
75
+ setView('discover');
45
76
  setCount(0);
46
77
  host.go(path({ kind: 'customize', tab: next }));
47
78
  };
48
79
  const pane = { view, q, adding, onAdding: setAdding, onView: setView, onCount };
49
80
  const kit = ORG_KIT.includes(tab);
50
81
  const may = Boolean(host.person) && (host.admin || !kit);
51
- 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: host.sections ? LABEL[tab].title : 'Customize' }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.sections ? LABEL[tab].says : 'What your agent can use in every run.' })] }), host.sections ? null : (_jsx(XStack, { role: "tablist", "aria-label": "Customize", gap: "$4", borderBottomWidth: 1, borderColor: "$borderColor", "$max-md": { gap: '$3' }, children: TABS.map((id) => {
82
+ 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: host.sections ? t(LABEL[tab].title) : t('Customize') }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.sections ? t(LABEL[tab].says) : t('What your agent can use in every run.') })] }), host.sections ? null : (_jsx(XStack, { role: "tablist", "aria-label": t('Customize'), gap: "$4", borderBottomWidth: 1, borderColor: "$borderColor", "$max-md": { gap: '$3' }, children: TABS.map((id) => {
52
83
  const on = id === tab;
53
- 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));
54
- }) })), host.sections ? null : (_jsx(SizableText, { size: "$2", color: "$soft", children: LABEL[tab].says })), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Choice, { label: "Show", value: view, options: [
55
- { id: 'discover', label: 'Browse' },
56
- { id: 'yours', label: 'Yours', count },
57
- ], 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 }))] }) }));
84
+ 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: t(LABEL[id].title) })] }, id));
85
+ }) })), host.sections ? null : (_jsx(SizableText, { size: "$2", color: "$soft", children: t(LABEL[tab].says) })), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Choice, { label: t('Show'), value: view, options: [
86
+ { id: 'discover', label: t('Browse') },
87
+ { id: 'yours', label: t('Yours'), count },
88
+ ], onChange: setView }), _jsx(XStack, { flex: 1 }), may ? (_jsxs(Button, { size: "sm", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t(LABEL[tab].add)] })) : null] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: t(LABEL[tab].typing), "aria-label": t(LABEL[tab].find) }), host.person && kit && !host.admin && LABEL[tab].admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t(LABEL[tab].admin) })) : null, tab === 'skills' ? (_jsx(Skills, { ...pane })) : tab === 'connectors' ? (_jsx(Connectors, { ...pane })) : tab === 'plugins' ? (_jsx(Plugins, { ...pane })) : (_jsx(Agents, { ...pane }))] }) }));
58
89
  }
@@ -9,8 +9,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
9
9
  * Describe an API instead of writing the TypeScript and a model writes it; the
10
10
  * result opens to be read. A plugin carries no credential: it names the
11
11
  * connector whose credential it reads when it runs.
12
+ *
13
+ * Its words are the reader's language (@hanzo/i18n `t`); a plugin's name, its
14
+ * source and the bundler's reasons are drawn as given.
12
15
  */
13
16
  import { SizableText, XStack, YStack } from '@hanzo/gui';
17
+ import { useI18n } from '@hanzo/i18n/react';
14
18
  import { Puzzle } from '@hanzogui/lucide-icons-2';
15
19
  import { Button, Input, Textarea } from '@hanzo/ui';
16
20
  import { Code } from '@hanzo/ui/chat';
@@ -22,11 +26,12 @@ import { say } from './say.js';
22
26
  import { Choice, Confirm, day, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
23
27
  const kb = (n) => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
24
28
  export function Plugins({ view, q, adding, onAdding, onView, onCount }) {
29
+ const { t } = useI18n();
25
30
  const host = useHost();
26
- const t = useTarget();
31
+ const target = useTarget();
27
32
  const signed = Boolean(host.person);
28
- const mine = useLoad(signed ? () => authored(t) : null, [], [t, signed]);
29
- const mounts = useLoad(signed && view === 'discover' ? () => mounted(t) : null, [], [t, signed, view]);
33
+ const mine = useLoad(signed ? () => authored(target) : null, [], [target, signed]);
34
+ const mounts = useLoad(signed && view === 'discover' ? () => mounted(target) : null, [], [target, signed, view]);
30
35
  const [opened, setOpened] = useState(null);
31
36
  const [note, setNote] = useState('');
32
37
  useCount(mine.value.length, onCount);
@@ -37,27 +42,24 @@ export function Plugins({ view, q, adding, onAdding, onView, onCount }) {
37
42
  setNote(said);
38
43
  setOpened(p);
39
44
  } })) : null, opened ? (_jsx(Source, { plugin: opened, onClose: () => setOpened(null), onDeleted: () => {
40
- setNote(`${opened.name} is deleted`);
45
+ setNote(t('{name} is deleted', { name: opened.name }));
41
46
  setOpened(null);
42
47
  mine.reload();
43
48
  } })) : null] }));
44
49
  if (!signed) {
45
- return _jsx(Visitor, { children: view === 'discover' ? 'Sign in to browse plugins.' : 'Sign in to see your plugins.' });
50
+ return _jsx(Visitor, { children: view === 'discover' ? t('Sign in to browse plugins.') : t('Sign in to see your plugins.') });
46
51
  }
47
52
  if (view === 'discover') {
48
53
  const shown = mounts.value.filter((m) => matches(q, m.name, ...m.prefixes));
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] }));
54
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Line, { children: note }), _jsx(Part, { title: t('Build your own'), detail: t('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: t('Build a plugin') }) }) }), _jsx(Part, { title: t('Built in'), detail: t('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: t('Loading built-in plugins…') })) : !shown.length ? (_jsx(Soft, { children: mounts.value.length ? t('Nothing built in matches “{q}”.', { q: q.trim() }) : t('Nothing built in is listed.') })) : (_jsxs(_Fragment, { children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('{n} built in', { n: shown.length }) }), _jsx(Grid, { label: t('Mounted subsystems'), children: shown.map((m) => (_jsx(Tile, { title: m.name, detail: m.prefixes.join(' '), meta: m.enabled ? t('On') : t('Off'), mark: _jsx(Mark, { name: m.name, icon: _jsx(Puzzle, { size: 15 }) }) }, m.name))) })] })) }), sheets] }));
50
55
  }
51
56
  const shown = mine.value.filter((p) => matches(q, p.name, p.provider));
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] }));
57
+ return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: t('Loading your plugins…') })) : !mine.value.length ? (_jsx(Empty, { title: t('No plugins yet'), detail: t('Write a connector in TypeScript, or describe an API and Hanzo writes it.'), children: _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: t('Build a plugin') }) })) : !shown.length ? (_jsx(Soft, { children: t('No plugin of yours matches “{q}”.', { q: q.trim() }) })) : (_jsx(Grid, { label: t('Your plugins'), children: shown.map((p) => (_jsx(Tile, { title: p.name, detail: p.provider ? t('Reads the {provider} connector’s credential when it runs.', { provider: p.provider }) : t('Needs no credential.'), meta: day(p.built) ? t('Built {date}', { date: day(p.built) }) : undefined, mark: _jsx(Mark, { name: p.name }), onOpen: () => setOpened(p) }, p.id))) })), sheets] }));
53
58
  }
54
- const WAYS = [
55
- { id: 'source', label: 'TypeScript' },
56
- { id: 'spec', label: 'Describe an API' },
57
- ];
58
59
  /** Build a plugin from its TypeScript, or from a description of the API it calls. */
59
60
  function Build({ onClose, onBuilt }) {
60
- const t = useTarget();
61
+ const { t } = useI18n();
62
+ const target = useTarget();
61
63
  const [name, setName] = useState('');
62
64
  const [provider, setProvider] = useState('');
63
65
  const [way, setWay] = useState('source');
@@ -68,28 +70,38 @@ function Build({ onClose, onBuilt }) {
68
70
  const [note, setNote] = useState('');
69
71
  const go = async () => {
70
72
  setWorking(true);
71
- setNote(way === 'spec' ? 'Writing and building it…' : 'Building it…');
73
+ setNote(way === 'spec' ? t('Writing and building it…') : t('Building it…'));
72
74
  try {
73
- const out = await build(t, { name: name.trim(), provider, source: way === 'source' ? source : '', spec: way === 'spec' ? spec : '' });
74
- onBuilt(out.plugin, `${out.plugin.name} built: ${kb(out.bytes)}${out.generated ? ', written from your description — read it below' : ''}.`);
75
+ const out = await build(target, { name: name.trim(), provider, source: way === 'source' ? source : '', spec: way === 'spec' ? spec : '' });
76
+ const built = { name: out.plugin.name, size: kb(out.bytes) };
77
+ onBuilt(out.plugin, out.generated ? t('{name} built: {size}, written from your description — read it below.', built) : t('{name} built: {size}.', built));
75
78
  }
76
79
  catch (e) {
77
- setNote(say(e));
80
+ setNote(say(e, t));
78
81
  }
79
82
  finally {
80
83
  setWorking(false);
81
84
  }
82
85
  };
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" })] })] }));
86
+ return (_jsxs(Sheet, { title: t('Build a plugin'), open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: t('Name'), hint: t('One lowercase word: letters, digits, _ or -.'), children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": t('Name'), autoCapitalize: "none" }) }), _jsx(Field, { label: t('Connector'), hint: t('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": t('Connector'), autoCapitalize: "none" }) }), _jsx(Choice, { label: t('Build from'), value: way, options: [
87
+ { id: 'source', label: 'TypeScript' },
88
+ { id: 'spec', label: t('Describe an API') },
89
+ ], onChange: setWay }), _jsx(Field, { label: way === 'source' ? t('Source') : t('The API'), hint: way === 'source'
90
+ ? t('Kept only if it builds. A failed build says why.')
91
+ : t('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' ? t('Source') : t('The API'), placeholder: way === 'source' ? 'export const acme = createProvider({ … })' : t('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: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: t('Build') })] })] }));
86
92
  }
87
93
  /** A built plugin's source, as the runtime will run it, and deleting it. */
88
94
  function Source({ plugin, onClose, onDeleted }) {
89
- const t = useTarget();
95
+ const { t } = useI18n();
96
+ const target = useTarget();
90
97
  const [asking, setAsking] = useState(false);
91
- return (_jsxs(Sheet, { title: plugin.name, open: true, onOpenChange: (o) => !o && onClose(), width: 760, children: [_jsxs(SizableText, { size: "$2", color: "$soft", children: [plugin.provider ? `Reads the ${plugin.provider} connector’s credential when it runs.` : 'Needs no credential.', day(plugin.built) ? ` Built ${day(plugin.built)}.` : ''] }), _jsx(Code, { language: "TypeScript", value: plugin.source, children: _jsx(SizableText, { size: "$1", color: "$ink", style: { ...mono, whiteSpace: 'pre' }, children: plugin.source }) }), _jsx(YStack, { items: "flex-start", children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete plugin" }) }), _jsx(Confirm, { what: plugin.name, says: "The runtime can no longer load it.", open: asking, onOpenChange: setAsking, onYes: async () => {
92
- await remove(t, plugin.id);
98
+ return (_jsxs(Sheet, { title: plugin.name, open: true, onOpenChange: (o) => !o && onClose(), width: 760, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: [
99
+ plugin.provider ? t('Reads the {provider} connector’s credential when it runs.', { provider: plugin.provider }) : t('Needs no credential.'),
100
+ day(plugin.built) ? t('Built {date}.', { date: day(plugin.built) }) : '',
101
+ ]
102
+ .filter(Boolean)
103
+ .join(' ') }), _jsx(Code, { language: "TypeScript", value: plugin.source, children: _jsx(SizableText, { size: "$1", color: "$ink", style: { ...mono, whiteSpace: 'pre' }, children: plugin.source }) }), _jsx(YStack, { items: "flex-start", children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: t('Delete plugin') }) }), _jsx(Confirm, { what: plugin.name, says: t('The runtime can no longer load it.'), open: asking, onOpenChange: setAsking, onYes: async () => {
104
+ await remove(target, plugin.id);
93
105
  onDeleted();
94
106
  } })] }));
95
107
  }
@@ -1 +1,13 @@
1
- export declare function say(e: unknown): string;
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
+ * Said in the reader's language: the screen hands its `t` (English without
9
+ * one). A sentence a module wrote is English and marked, so it goes through
10
+ * `t`; the platform's own words are no key and come back from it as given.
11
+ */
12
+ import type { Say } from '@hanzo/i18n/react';
13
+ export declare function say(e: unknown, t?: Say): string;
@@ -1,38 +1,32 @@
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';
1
+ import { Refusal, told } from '../api/call.js';
9
2
  import { Slow } from '../data.js';
3
+ import { english } from '../say.js';
10
4
  /** The line call.ts writes when the platform gave no reason: a method, a path and a status. */
11
5
  const BARE = /^(GET|POST|PUT|PATCH|DELETE) \S* answered \d+$/;
12
- export function say(e) {
6
+ export function say(e, t = english) {
13
7
  if (e instanceof Slow)
14
- return 'Hanzo is taking too long to answer. Try again.';
8
+ return t('Hanzo is taking too long to answer. Try again.');
15
9
  if (e instanceof Refusal) {
16
10
  if (e.status === 401)
17
- return 'Your sign-in has expired. Sign in again.';
11
+ return t('Your sign-in has expired. Sign in again.');
18
12
  if (e.status === 403)
19
- return 'Your account can’t do this here. Ask an org admin.';
13
+ return t('Your account can’t do this here. Ask an org admin.');
20
14
  if (e.status === 404)
21
- return 'Not found. It may have been removed.';
15
+ return t('Not found. It may have been removed.');
22
16
  if (e.status === 429)
23
- return 'Too many requests. Wait a moment and try again.';
17
+ return t('Too many requests. Wait a moment and try again.');
24
18
  if (e.status >= 500)
25
- return 'Hanzo could not answer just now. Try again.';
19
+ return t('Hanzo could not answer just now. Try again.');
26
20
  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.';
21
+ return t(e.message);
22
+ return t('Hanzo could not do that. Try again.');
29
23
  }
30
24
  // fetch rejects with a TypeError when the network does; a body that is not JSON is a SyntaxError.
31
25
  if (e instanceof TypeError)
32
- return 'Could not reach Hanzo. Check your connection and try again.';
26
+ return t('Could not reach Hanzo. Check your connection and try again.');
33
27
  if (e instanceof SyntaxError)
34
- return 'Hanzo answered with something unreadable. Try again.';
28
+ return t('Hanzo answered with something unreadable. Try again.');
35
29
  if (e instanceof Error && e.message)
36
- return e.message;
37
- return 'Something went wrong. Try again.';
30
+ return told(e, t);
31
+ return t('Something went wrong. Try again.');
38
32
  }