@hanzo/build 0.2.80 → 0.2.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +13 -0
  2. package/lib/activity.d.ts +3 -7
  3. package/lib/activity.js +18 -5
  4. package/lib/api/agents.d.ts +1 -20
  5. package/lib/api/agents.js +16 -8
  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.js +78 -31
  67. package/lib/customize/connectors.js +62 -48
  68. package/lib/customize/index.js +43 -10
  69. package/lib/customize/plugins.js +34 -22
  70. package/lib/customize/say.d.ts +13 -1
  71. package/lib/customize/say.js +15 -21
  72. package/lib/customize/skills.js +31 -22
  73. package/lib/customize/ui.d.ts +5 -9
  74. package/lib/customize/ui.js +26 -16
  75. package/lib/data.d.ts +1 -1
  76. package/lib/data.js +3 -2
  77. package/lib/desk.js +45 -33
  78. package/lib/door.js +35 -10
  79. package/lib/environment.js +25 -19
  80. package/lib/files.js +43 -34
  81. package/lib/find.js +12 -9
  82. package/lib/foot.js +7 -4
  83. package/lib/forge.js +61 -40
  84. package/lib/gear.d.ts +2 -1
  85. package/lib/gear.js +6 -4
  86. package/lib/git.js +29 -19
  87. package/lib/landing.d.ts +5 -5
  88. package/lib/landing.js +52 -44
  89. package/lib/library.js +73 -51
  90. package/lib/meter.d.ts +1 -1
  91. package/lib/meter.js +14 -5
  92. package/lib/mic.js +12 -5
  93. package/lib/mind.d.ts +1 -0
  94. package/lib/mind.js +5 -3
  95. package/lib/nav.d.ts +6 -5
  96. package/lib/nav.js +37 -30
  97. package/lib/panel.d.ts +2 -1
  98. package/lib/panel.js +52 -36
  99. package/lib/plan.d.ts +4 -20
  100. package/lib/plan.js +26 -10
  101. package/lib/plans.js +49 -30
  102. package/lib/plugins/design.js +3 -2
  103. package/lib/plugins/index.d.ts +23 -4
  104. package/lib/plugins/index.js +11 -26
  105. package/lib/plugins/pdf.js +2 -1
  106. package/lib/plugins/project.js +6 -3
  107. package/lib/plugins/research.js +3 -2
  108. package/lib/plugins/sketch.js +6 -3
  109. package/lib/plugins/slides.js +3 -2
  110. package/lib/plugins/template.js +6 -3
  111. package/lib/plus.js +25 -13
  112. package/lib/prefs.d.ts +0 -14
  113. package/lib/prefs.js +2 -1
  114. package/lib/project.js +69 -62
  115. package/lib/prompt.d.ts +12 -11
  116. package/lib/prompt.js +38 -20
  117. package/lib/publish.js +17 -12
  118. package/lib/reply.js +6 -3
  119. package/lib/run.js +72 -54
  120. package/lib/say.d.ts +2 -0
  121. package/lib/say.js +7 -0
  122. package/lib/scheduled.js +54 -43
  123. package/lib/section.d.ts +1 -0
  124. package/lib/section.js +8 -2
  125. package/lib/settings/account.d.ts +1 -0
  126. package/lib/settings/account.js +31 -21
  127. package/lib/settings/billing.js +55 -28
  128. package/lib/settings/capabilities.js +26 -15
  129. package/lib/settings/card.js +19 -12
  130. package/lib/settings/code.js +22 -11
  131. package/lib/settings/credits.d.ts +4 -4
  132. package/lib/settings/credits.js +34 -21
  133. package/lib/settings/environments.js +21 -14
  134. package/lib/settings/general.js +26 -14
  135. package/lib/settings/index.js +6 -3
  136. package/lib/settings/integrations.js +42 -28
  137. package/lib/settings/keys.js +37 -19
  138. package/lib/settings/list.d.ts +0 -5
  139. package/lib/settings/list.js +24 -15
  140. package/lib/settings/machines.d.ts +3 -0
  141. package/lib/settings/machines.js +63 -38
  142. package/lib/settings/members.js +23 -14
  143. package/lib/settings/memory.js +12 -6
  144. package/lib/settings/notifications.js +32 -19
  145. package/lib/settings/plan.d.ts +1 -1
  146. package/lib/settings/plan.js +40 -17
  147. package/lib/settings/privacy.js +26 -13
  148. package/lib/settings/ui.d.ts +3 -2
  149. package/lib/settings/ui.js +9 -3
  150. package/lib/settings/usage.js +7 -3
  151. package/lib/shelf.js +18 -15
  152. package/lib/side.d.ts +1 -26
  153. package/lib/side.js +11 -5
  154. package/lib/standing.d.ts +2 -10
  155. package/lib/standing.js +5 -3
  156. package/lib/suggest.d.ts +8 -28
  157. package/lib/suggest.js +39 -4
  158. package/lib/suggestions.js +14 -5
  159. package/lib/summary.d.ts +1 -1
  160. package/lib/summary.js +4 -1
  161. package/lib/sync.js +22 -12
  162. package/lib/tools.d.ts +4 -3
  163. package/lib/tools.js +10 -7
  164. package/lib/transcript.js +10 -8
  165. package/package.json +4 -2
@@ -5,55 +5,69 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * own credential: creating one never touches another, and revoking one stops only
6
6
  * that one. A secret key is shown once, when it is made. A limit narrows what a
7
7
  * key reaches; it never reaches further than you do.
8
+ *
9
+ * Its words are the reader's language (@hanzo/i18n `t`); a key's name, its
10
+ * prefix and its kind:name limits are drawn as given.
8
11
  */
9
12
  import { SizableText, XStack, YStack } from '@hanzo/gui';
13
+ import { phrase } from '@hanzo/i18n';
14
+ import { useI18n } from '@hanzo/i18n/react';
10
15
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
11
16
  import { CopyButton } from '@hanzo/ui/product';
12
17
  import { useState } from 'react';
18
+ import { told } from '../api/call.js';
13
19
  import { keys, KINDS, limits, mint, revoke } from '../api/keys.js';
14
20
  import { Confirm } from '../ask.js';
15
21
  import { useRead } from '../data.js';
16
22
  import { useHost, useTarget } from '../host.js';
17
23
  import { Card, Field, Heading, Note, Once, Row, Soft } from './ui.js';
18
- const NAME = { secret: 'Secret key', publishable: 'Publishable key' };
24
+ /** Each kind's words, whole, so a language says the kind its own way: its name, its create control and label, and the dialog's titles. */
25
+ const NAME = { secret: phrase('Secret key'), publishable: phrase('Publishable key') };
26
+ const CREATE = { secret: phrase('Create secret key'), publishable: phrase('Create publishable key') };
27
+ const CREATE_A = { secret: phrase('Create a secret key'), publishable: phrase('Create a publishable key') };
28
+ const NEW = { secret: phrase('Your new secret key'), publishable: phrase('Your new publishable key') };
29
+ /** The statuses IAM names besides active, as words; one it does not is drawn as given. */
30
+ const STATUS = { expired: phrase('Expired'), revoked: phrase('Revoked'), disabled: phrase('Disabled') };
19
31
  /** The line under a key: its head, what it reaches, when it was made, and where it stands. */
20
- function about(k) {
32
+ function about(k, t) {
21
33
  const when = k.created ? new Date(k.created) : null;
22
34
  return [
23
35
  k.type === 'secret' && k.prefix ? `${k.prefix}…` : '',
24
- k.type === 'publishable' ? 'Publishable' : '',
25
- k.limit.length ? `Reaches ${k.limit.join(', ')}` : 'Unrestricted',
26
- when && !Number.isNaN(when.getTime()) ? `Created ${when.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}` : '',
27
- k.status !== 'active' ? k.status[0].toUpperCase() + k.status.slice(1) : '',
36
+ k.type === 'publishable' ? t('Publishable') : '',
37
+ k.limit.length ? t('Reaches {limits}', { limits: k.limit.join(', ') }) : t('Unrestricted'),
38
+ when && !Number.isNaN(when.getTime()) ? t('Created {date}', { date: when.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) }) : '',
39
+ k.status !== 'active' ? (STATUS[k.status] ? t(STATUS[k.status]) : k.status[0].toUpperCase() + k.status.slice(1)) : '',
28
40
  ]
29
41
  .filter(Boolean)
30
42
  .join(' · ');
31
43
  }
32
44
  export function Keys() {
45
+ const { t } = useI18n();
33
46
  const host = useHost();
34
- const t = useTarget();
47
+ const target = useTarget();
35
48
  const signed = Boolean(host.person);
36
- const list = useRead(signed ? () => keys(t) : null, [], [t, signed]);
49
+ const list = useRead(signed ? () => keys(target) : null, [], [target, signed]);
37
50
  const [making, setMaking] = useState(null);
38
51
  const [revoking, setRevoking] = useState(null);
39
52
  const [note, setNote] = useState('');
40
53
  if (!signed) {
41
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "API keys" }), _jsx(Soft, { children: "Sign in to see your API keys." })] }));
54
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('API keys') }), _jsx(Soft, { children: t('Sign in to see your API keys.') })] }));
42
55
  }
43
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "API keys", detail: "Your keys for calling api.hanzo.ai from code. Make one for each app; revoking one stops only that one." }), _jsx(XStack, { gap: "$2", children: KINDS.map((type) => (_jsx(Button, { size: "sm", variant: "outline", "aria-label": `Create a ${type} key`, onPress: () => setMaking(type), children: `Create ${NAME[type].toLowerCase()}` }, type))) }), list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading your keys\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No keys yet." })) : (_jsx(Card, { children: list.value.map((k, i) => (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", opacity: k.status === 'active' ? 1 : 0.55, children: [_jsx(Row, { first: true, title: k.name || NAME[k.type], detail: about(k), trailing: k.status === 'revoked' ? null : (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Revoke ${k.name || k.id}`, onPress: () => setRevoking(k), children: "Revoke" })) }), k.type === 'publishable' && k.key ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", pb: "$2.5", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, "aria-label": "Publishable key", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: k.key }), _jsx(CopyButton, { value: k.key, label: "Copy publishable key" })] })) : null] }, k.id))) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, onClose: () => setMaking(null), onMade: (m) => {
44
- setNote(`${m.name || NAME[m.type]} created`);
56
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('API keys'), detail: t('Your keys for calling {api} from code. Make one for each app; revoking one stops only that one.', { api: 'api.hanzo.ai' }) }), _jsx(XStack, { gap: "$2", children: KINDS.map((type) => (_jsx(Button, { size: "sm", variant: "outline", "aria-label": t(CREATE_A[type]), onPress: () => setMaking(type), children: t(CREATE[type]) }, type))) }), list.error ? (_jsx(Soft, { children: told(list.error, t) })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: t('Reading your keys…') })) : list.value.length === 0 ? (_jsx(Soft, { children: t('No keys yet.') })) : (_jsx(Card, { children: list.value.map((k, i) => (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", opacity: k.status === 'active' ? 1 : 0.55, children: [_jsx(Row, { first: true, title: k.name || t(NAME[k.type]), detail: about(k, t), trailing: k.status === 'revoked' ? null : (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Revoke {name}', { name: k.name || k.id }), onPress: () => setRevoking(k), children: t('Revoke') })) }), k.type === 'publishable' && k.key ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", pb: "$2.5", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, "aria-label": t('Publishable key'), style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: k.key }), _jsx(CopyButton, { value: k.key, label: t('Copy publishable key') })] })) : null] }, k.id))) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, onClose: () => setMaking(null), onMade: (m) => {
57
+ setNote(t('{name} created', { name: m.name || t(NAME[m.type]) }));
45
58
  list.reload();
46
- } }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: `Revoke ${revoking?.name || 'this key'}?`, says: "It stops working within a minute, and anything using it is refused. Your other keys keep working.", act: "Revoke",
59
+ } }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: revoking?.name ? t('Revoke {name}?', { name: revoking.name }) : t('Revoke this key?'), says: t('It stops working within a minute, and anything using it is refused. Your other keys keep working.'), act: t('Revoke'),
47
60
  // It acts only while it is open, which is while a key is chosen.
48
61
  run: async () => {
49
- await revoke(t, revoking.id);
50
- setNote(`${revoking.name || NAME[revoking.type]} revoked`);
62
+ await revoke(target, revoking.id);
63
+ setNote(t('{name} revoked', { name: revoking.name || t(NAME[revoking.type]) }));
51
64
  list.reload();
52
65
  } })] }));
53
66
  }
54
67
  /** Creating one key: a name and an optional limit, then the key itself, once. */
55
68
  function Making({ type, onClose, onMade }) {
56
- const t = useTarget();
69
+ const { t } = useI18n();
70
+ const target = useTarget();
57
71
  const [name, setName] = useState('');
58
72
  const [limit, setLimit] = useState('');
59
73
  const [made, setMade] = useState(null);
@@ -76,21 +90,25 @@ function Making({ type, onClose, onMade }) {
76
90
  list = limits(limit);
77
91
  }
78
92
  catch (e) {
79
- setNote(e.message);
93
+ setNote(told(e, t));
80
94
  return;
81
95
  }
82
96
  setWorking(true);
83
97
  try {
84
- const m = await mint(t, type, name, list);
98
+ const m = await mint(target, type, name, list);
85
99
  setMade(m);
86
100
  onMade(m);
87
101
  }
88
102
  catch (e) {
89
- setNote(e.message);
103
+ setNote(told(e, t));
90
104
  }
91
105
  finally {
92
106
  setWorking(false);
93
107
  }
94
108
  };
95
- return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? `Your new ${made.type} key` : `Create a ${type ?? ''} key` }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: "Secret key" })) : (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: made.key }), _jsx(CopyButton, { value: made.key, label: "Copy publishable key" })] })), made.limit.length ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["It reaches ", made.limit.join(', '), " and nothing else."] })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: "Done" }) })] })) : (_jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", hint: "What it is for, so you can tell it from your other keys.", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "production", "aria-label": "Name" }) }), _jsx(Field, { label: "Limit", hint: "Optional. What it may reach, as kind:name \u2014 model:zen5, project:acme, product:train, read:* for read-only. Empty reaches whatever you do.", children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": "Limit", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(type), children: "Create" })] })] }))] }) }));
109
+ return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? t(NEW[made.type]) : type ? t(CREATE_A[type]) : '' }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: t('Secret key'), copy: t('Copy secret key') })) : (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: made.key }), _jsx(CopyButton, { value: made.key, label: t('Copy publishable key') })] })), made.limit.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('It reaches {limits} and nothing else.', { limits: made.limit.join(', ') }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: t('Done') }) })] })) : (_jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: t('Name'), hint: t('What it is for, so you can tell it from your other keys.'), children: _jsx(Input, { value: name, onChangeText: setName, placeholder: t('production'), "aria-label": t('Name') }) }), _jsx(Field, { label: t('Limit'), hint: t('Optional. What it may reach, as {form} — {examples}, {read} for read-only. Empty reaches whatever you do.', {
110
+ form: 'kind:name',
111
+ examples: 'model:zen5, project:acme, product:train',
112
+ read: 'read:*',
113
+ }), children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": t('Limit'), autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(type), children: t('Create') })] })] }))] }) }));
96
114
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * The sections of Settings, in the order the page lists them, by name and part
3
- * alone: what a sidebar lists without loading the sections themselves
4
- * (sections.tsx pairs each with its body).
5
- */
6
1
  import type { Section } from '../route.ts';
7
2
  export type Group = 'Settings' | 'Code' | 'Organization';
8
3
  export declare const GROUPS: readonly Group[];
@@ -1,19 +1,28 @@
1
- export const GROUPS = ['Settings', 'Code', 'Organization'];
1
+ /**
2
+ * The sections of Settings, in the order the page lists them, by name and part
3
+ * alone: what a sidebar lists without loading the sections themselves
4
+ * (sections.tsx pairs each with its body).
5
+ *
6
+ * A part's id is its name, and every name is English, marked (`phrase`): a
7
+ * list draws each through `t`.
8
+ */
9
+ import { phrase } from '@hanzo/i18n';
10
+ export const GROUPS = [phrase('Settings'), phrase('Code'), phrase('Organization')];
2
11
  export const LIST = [
3
- { id: 'general', label: 'General', group: 'Settings' },
4
- { id: 'account', label: 'Account', group: 'Settings' },
5
- { id: 'privacy', label: 'Privacy', group: 'Settings' },
6
- { id: 'billing', label: 'Billing', group: 'Settings' },
7
- { id: 'usage', label: 'Usage', group: 'Settings' },
8
- { id: 'capabilities', label: 'Capabilities', group: 'Settings' },
9
- { id: 'memory', label: 'Memory', group: 'Settings' },
10
- { id: 'code', label: 'Code', group: 'Code' },
11
- { id: 'environments', label: 'Environments', group: 'Code' },
12
- { id: 'machines', label: 'Machines', group: 'Code' },
13
- { id: 'keys', label: 'API keys', group: 'Code' },
14
- { id: 'members', label: 'Members', group: 'Organization' },
15
- { id: 'integrations', label: 'Integrations', group: 'Organization' },
16
- { id: 'notifications', label: 'Notifications', group: 'Organization' },
12
+ { id: 'general', label: phrase('General'), group: 'Settings' },
13
+ { id: 'account', label: phrase('Account'), group: 'Settings' },
14
+ { id: 'privacy', label: phrase('Privacy'), group: 'Settings' },
15
+ { id: 'billing', label: phrase('Billing'), group: 'Settings' },
16
+ { id: 'usage', label: phrase('Usage'), group: 'Settings' },
17
+ { id: 'capabilities', label: phrase('Capabilities'), group: 'Settings' },
18
+ { id: 'memory', label: phrase('Memory'), group: 'Settings' },
19
+ { id: 'code', label: phrase('Code'), group: 'Code' },
20
+ { id: 'environments', label: phrase('Environments'), group: 'Code' },
21
+ { id: 'machines', label: phrase('Machines'), group: 'Code' },
22
+ { id: 'keys', label: phrase('API keys'), group: 'Code' },
23
+ { id: 'members', label: phrase('Members'), group: 'Organization' },
24
+ { id: 'integrations', label: phrase('Integrations'), group: 'Organization' },
25
+ { id: 'notifications', label: phrase('Notifications'), group: 'Organization' },
17
26
  ];
18
27
  /** A part's label as a list draws it: none for the first part, or where the part's first row already says it (Code). */
19
28
  export const heading = (g) => (g === 'Settings' || LIST.find((e) => e.group === g)?.label === g ? '' : g);
@@ -1 +1,4 @@
1
+ import { type Say } from '@hanzo/i18n/react';
2
+ /** A status as a line says it. Code's place list says it the same way. */
3
+ export declare const said: (status: string, t: Say) => string;
1
4
  export declare function Machines(): import("react").JSX.Element;
@@ -5,12 +5,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * and keeps it online while it runs; one can also be registered here by hand.
6
6
  * The member who linked a machine, or an org admin, renames it, drains it,
7
7
  * mints its claim key or removes it.
8
+ *
9
+ * Its words are the reader's language (@hanzo/i18n `t`); a machine's name,
10
+ * hostname, id and hardware, and the commands to type, are drawn as given.
8
11
  */
9
12
  import { SizableText, XStack, YStack } from '@hanzo/gui';
13
+ import { phrase } from '@hanzo/i18n';
14
+ import { useI18n } from '@hanzo/i18n/react';
10
15
  import { Activity, ChevronDown, ChevronUp, Cloud, Cpu, HardDrive, Laptop, MoreHorizontal, Plus, Server, Terminal, Zap, } from '@hanzogui/lucide-icons-2';
11
16
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
12
17
  import { CopyButton } from '@hanzo/ui/product';
13
18
  import { useEffect, useState } from 'react';
19
+ import { told } from '../api/call.js';
14
20
  import { add, change, formatBytes, formatPercent, formatRelative, key, KINDS, machines, remove, state, } from '../api/machines.js';
15
21
  import { Confirm, Rename } from '../ask.js';
16
22
  import { useRead } from '../data.js';
@@ -23,10 +29,23 @@ const LINK = ['hanzo login', 'hanzo link'].join('\n');
23
29
  const BEAT = 30_000;
24
30
  /** The list before its first answer; every answer is a new array, even an empty one. */
25
31
  const UNREAD = [];
32
+ /** A machine's status in a line, the platform's word or `not seen yet`, said in the reader's language; one it does not name, as given. */
33
+ const STATES = {
34
+ online: phrase('online'),
35
+ offline: phrase('offline'),
36
+ draining: phrase('draining'),
37
+ 'not seen yet': phrase('not seen yet'),
38
+ '': phrase('unknown'),
39
+ };
40
+ /** What a machine is, as the platform names its kinds, said in the reader's language; one it does not name, as given. */
41
+ const KIND = { laptop: phrase('laptop'), gpu: phrase('gpu'), cloud: phrase('cloud'), cluster: phrase('cluster'), machine: phrase('machine') };
42
+ /** A status as a line says it. Code's place list says it the same way. */
43
+ export const said = (status, t) => (status in STATES ? t(STATES[status]) : status);
44
+ const kindOf = (kind, t) => (KIND[kind] ? t(KIND[kind]) : kind);
26
45
  /** The line under a machine: what it is, where, how big, and what it is doing. */
27
- function about(m) {
28
- const load = m.running ? `${m.running} running` : m.sessions ? `${m.sessions} runs` : '';
29
- return [state(m) || 'unknown', m.kind, m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
46
+ function about(m, t) {
47
+ const load = m.running ? t('{n} running', { n: m.running }) : m.sessions ? t('{n} runs', { n: m.sessions }) : '';
48
+ return [said(state(m), t), kindOf(m.kind, t), m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
30
49
  }
31
50
  function Dot({ m }) {
32
51
  const on = state(m) === 'online';
@@ -47,6 +66,7 @@ function KindIcon({ kind, size = 16 }) {
47
66
  }
48
67
  }
49
68
  function StatusBadge({ m }) {
69
+ const { t } = useI18n();
50
70
  const s = state(m);
51
71
  if (!s)
52
72
  return null;
@@ -54,27 +74,28 @@ function StatusBadge({ m }) {
54
74
  let text = s;
55
75
  if (s === 'online') {
56
76
  color = '$green10';
57
- text = 'Online';
77
+ text = t('Online');
58
78
  }
59
79
  else if (s === 'draining') {
60
80
  color = '$yellow10';
61
- text = 'Draining';
81
+ text = t('Draining');
62
82
  }
63
83
  else if (s === 'not seen yet') {
64
84
  color = '$blue10';
65
- text = 'Not seen yet';
85
+ text = t('Not seen yet');
66
86
  }
67
87
  else if (s === 'offline') {
68
88
  color = '$soft';
69
- text = 'Offline';
89
+ text = t('Offline');
70
90
  }
71
91
  return (_jsxs(XStack, { px: "$2", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: color, items: "center", gap: "$1.5", children: [_jsx(Dot, { m: m }), _jsx(SizableText, { size: "$1", color: color, style: { fontWeight: 500 }, children: text })] }));
72
92
  }
73
93
  export function Machines() {
94
+ const { t } = useI18n();
74
95
  const host = useHost();
75
- const t = useTarget();
96
+ const target = useTarget();
76
97
  const signed = Boolean(host.person);
77
- const list = useRead(signed ? () => machines(t) : null, UNREAD, [t, signed]);
98
+ const list = useRead(signed ? () => machines(target) : null, UNREAD, [target, signed]);
78
99
  const [adding, setAdding] = useState(false);
79
100
  const [renaming, setRenaming] = useState(null);
80
101
  const [removing, setRemoving] = useState(null);
@@ -92,53 +113,57 @@ export function Machines() {
92
113
  return () => clearInterval(id);
93
114
  }, [again, signed]);
94
115
  if (!signed) {
95
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Machines" }), _jsx(Soft, { children: "Sign in to see your organization\u2019s machines." })] }));
116
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Machines') }), _jsx(Soft, { children: t('Sign in to see your organization’s machines.') })] }));
96
117
  }
97
118
  const drain = async (m, status) => {
98
119
  setNote('');
99
120
  try {
100
- await change(t, m.id, { status });
101
- setNote(status === 'draining' ? `${m.label} takes no new runs` : `${m.label} takes runs again`);
121
+ await change(target, m.id, { status });
122
+ setNote(status === 'draining' ? t('{name} takes no new runs', { name: m.label }) : t('{name} takes runs again', { name: m.label }));
102
123
  list.reload();
103
124
  }
104
125
  catch (e) {
105
- setNote(e.message);
126
+ setNote(told(e, t));
106
127
  }
107
128
  };
108
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsxs(XStack, { items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }), _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setShowAdvancedLink(!showAdvancedLink), children: [showAdvancedLink ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), showAdvancedLink ? 'Hide options' : 'More connection options'] })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Supported: macOS, Linux, Windows, NVIDIA CUDA GPUs, Cloud VMs" })] }), showAdvancedLink ? (_jsxs(YStack, { gap: "$2.5", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: "1. Background daemon (system service)" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Run worker continuously in the background without keeping a terminal open:" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: "hanzo code --serve" }), _jsx(CopyButton, { value: "hanzo code --serve", label: "Copy daemon command" })] })] }), _jsxs(YStack, { gap: "$1", pt: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: "2. Install CLI from shell" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: "curl -fsSL https://hanzo.sh | sh" }), _jsx(CopyButton, { value: "curl -fsSL https://hanzo.sh | sh", label: "Copy install command" })] })] })] })) : null] }), _jsxs(Group, { title: "Linked", children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? list.error.message : 'Reading machines…' })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Soft, { children: "No machine is linked yet." }), _jsx(Card, { children: _jsxs(YStack, { p: "$4", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(Server, { size: 20 }), _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: "Connect your first computer or GPU server" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Execute code, runs, and local models directly on your hardware with full privacy and zero cloud fees." })] })] }), _jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "1." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Install Hanzo CLI: ", _jsx("span", { style: mono, children: "curl -fsSL https://hanzo.sh | sh" })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "2." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Sign in with your account: ", _jsx("span", { style: mono, children: "hanzo login" })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "3." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Link the machine: ", _jsx("span", { style: mono, children: "hanzo link" })] })] })] }), _jsx(XStack, { pt: "$1", children: _jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }) })] })) : (_jsx(Card, { children: list.value.map((m, i) => {
129
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Machines'), detail: t('Your organization’s own computers. A run sent to one runs there instead of in the Hanzo sandbox.') }), _jsxs(Group, { title: t('Link a machine'), detail: t('Run these on it. It registers under its hostname, and stays online while {command} runs.', { command: 'hanzo link' }), children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": t('Link commands'), 'data-i18n-ltr': '', fontFamily: "$mono", "$platform-web": { whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: t('Copy the commands') })] }), _jsxs(XStack, { items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t('Register one by hand')] }), _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setShowAdvancedLink(!showAdvancedLink), children: [showAdvancedLink ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), showAdvancedLink ? t('Hide options') : t('More connection options')] })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Supported: macOS, Linux, Windows, NVIDIA CUDA GPUs, Cloud VMs') })] }), showAdvancedLink ? (_jsxs(YStack, { gap: "$2.5", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: t('1. Background daemon (system service)') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Run worker continuously in the background without keeping a terminal open:') }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: "hanzo code --serve" }), _jsx(CopyButton, { value: "hanzo code --serve", label: t('Copy daemon command') })] })] }), _jsxs(YStack, { gap: "$1", pt: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: t('2. Install CLI from shell') }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: "curl -fsSL https://hanzo.sh | sh" }), _jsx(CopyButton, { value: "curl -fsSL https://hanzo.sh | sh", label: t('Copy install command') })] })] })] })) : null] }), _jsxs(Group, { title: t('Linked'), children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? told(list.error, t) : t('Reading machines…') })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Soft, { children: t('No machine is linked yet.') }), _jsx(Card, { children: _jsxs(YStack, { p: "$4", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(Server, { size: 20 }), _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: t('Connect your first computer or GPU server') }), _jsx(SizableText, { size: "$2", color: "$soft", children: t('Execute code, runs, and local models directly on your hardware with full privacy and zero cloud fees.') })] })] }), _jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "1." }), _jsx(SizableText, { size: "$2", color: "$ink", children: t('Install Hanzo CLI: {command}', { command: _jsx(SizableText, { size: "$2", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "curl -fsSL https://hanzo.sh | sh" }) }) })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "2." }), _jsx(SizableText, { size: "$2", color: "$ink", children: t('Sign in with your account: {command}', { command: _jsx(SizableText, { size: "$2", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo login" }) }) })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "3." }), _jsx(SizableText, { size: "$2", color: "$ink", children: t('Link the machine: {command}', { command: _jsx(SizableText, { size: "$2", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo link" }) }) })] })] }), _jsx(XStack, { pt: "$1", children: _jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t('Register one by hand')] }) })] }) })] })) : (_jsx(Card, { children: list.value.map((m, i) => {
109
130
  const isExpanded = expandedId === m.id;
110
131
  const hasTelemetry = Boolean(m.metrics || m.spec);
111
- return (_jsxs(YStack, { borderTopWidth: i > 0 ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: i === 0, leading: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Dot, { m: m }), _jsx(KindIcon, { kind: m.kind, size: 16 })] }), title: m.label, detail: about(m), trailing: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusBadge, { m: m }), m.serving ? (_jsx(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: "$green10", children: _jsx(SizableText, { size: "$1", color: "$green10", children: "Serving" }) })) : null, m.running > 0 ? (_jsxs(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", bg: "$raised", items: "center", gap: "$1", children: [_jsx(Activity, { size: 12, color: "$green10" }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.running, " active"] })] })) : null, _jsx(CopyButton, { value: `hanzo link --target ${m.id}`, label: `Copy link command for ${m.label}` }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
112
- { key: 'details', label: isExpanded ? 'Hide details' : 'Show details', onSelect: () => setExpandedId(isExpanded ? null : m.id) },
113
- { key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
132
+ return (_jsxs(YStack, { borderTopWidth: i > 0 ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: i === 0, leading: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Dot, { m: m }), _jsx(KindIcon, { kind: m.kind, size: 16 })] }), title: m.label, detail: about(m, t), trailing: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusBadge, { m: m }), m.serving ? (_jsx(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: "$green10", children: _jsx(SizableText, { size: "$1", color: "$green10", children: t('Serving') }) })) : null, m.running > 0 ? (_jsxs(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", bg: "$raised", items: "center", gap: "$1", children: [_jsx(Activity, { size: 12, color: "$green10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: t('{n} active', { n: m.running }) })] })) : null, _jsx(CopyButton, { value: `hanzo link --target ${m.id}`, label: t('Copy link command for {name}', { name: m.label }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": t('Actions for {name}', { name: m.label }), px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
133
+ { key: 'details', label: isExpanded ? t('Hide details') : t('Show details'), onSelect: () => setExpandedId(isExpanded ? null : m.id) },
134
+ { key: 'rename', label: t('Rename'), onSelect: () => setRenaming(m) },
114
135
  m.status === 'draining'
115
- ? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
116
- : { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
117
- { key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
136
+ ? { key: 'online', label: t('Take runs again'), onSelect: () => void drain(m, 'online') }
137
+ : { key: 'drain', label: t('Drain'), description: t('Finish what it has, take nothing new'), onSelect: () => void drain(m, 'draining') },
138
+ { key: 'key', label: t('Claim key'), onSelect: () => setKeying(m) },
118
139
  { type: 'separator' },
119
- { key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
120
- ] })] }) }), isExpanded || hasTelemetry ? (_jsxs(YStack, { px: "$4", pb: "$3", pt: "$1", gap: "$2.5", children: [m.spec ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$2", items: "center", children: [m.spec.os ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Terminal, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.os, m.spec.arch ? ` · ${m.spec.arch}` : ''] })] })) : null, m.spec.cpus ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Cpu, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.cpus, " cores"] })] })) : null, m.spec.memory ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(HardDrive, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [formatBytes(m.spec.memory), " RAM"] })] })) : null, m.spec.gpus && m.spec.gpus.length > 0 ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Zap, { size: 12, color: "$yellow10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: m.spec.gpus.map((g) => `${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`).join(', ') })] })) : null] })) : null, m.metrics && (m.metrics.cpuUtil || m.metrics.load1 || m.metrics.memUsed || m.metrics.gpuUtil) ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$3", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", children: [m.metrics.cpuUtil ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "CPU Utilization" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.cpuUtil), " ", m.metrics.cpuTemp ? `· ${m.metrics.cpuTemp}°C` : ''] })] })) : m.metrics.load1 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Load (1m, 5m, 15m)" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.load1, ", ", m.metrics.load5 ?? 0, ", ", m.metrics.load15 ?? 0] })] })) : null, m.metrics.memUsed ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Memory" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatBytes(m.metrics.memUsed), " ", m.spec?.memory ? `/ ${formatBytes(m.spec.memory)}` : 'used'] })] })) : null, m.metrics.gpuUtil !== undefined && m.metrics.gpuUtil > 0 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "GPU" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.gpuUtil), " ", m.metrics.gpuTemp ? `· ${m.metrics.gpuTemp}°C` : '', " ", m.metrics.gpuPower ? `· ${m.metrics.gpuPower}W` : ''] })] })) : null, m.metrics.model ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Serving Model" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.model, " ", m.metrics.decode ? `(${m.metrics.decode} tok/s)` : ''] })] })) : null] })) : null, _jsxs(XStack, { justify: "space-between", items: "center", flexWrap: "wrap", gap: "$2", pt: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(SizableText, { size: "$1", color: "$soft", style: mono, children: ["ID: ", m.id] }), m.seen ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Last heartbeat: ", formatRelative(m.seen)] })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Run directly on this machine:" }), _jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$0.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, children: ["hanzo run --target ", m.id] }), _jsx(CopyButton, { value: `hanzo run --target ${m.id}`, label: `Copy run command for ${m.label}` })] })] })] })] })) : null] }, m.id));
121
- }) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: list.error.message }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
122
- setNote(`${m.label} is registered`);
140
+ { key: 'remove', label: t('Remove'), destructive: true, onSelect: () => setRemoving(m) },
141
+ ] })] }) }), isExpanded || hasTelemetry ? (_jsxs(YStack, { px: "$4", pb: "$3", pt: "$1", gap: "$2.5", children: [m.spec ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$2", items: "center", children: [m.spec.os ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Terminal, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.os, m.spec.arch ? ` · ${m.spec.arch}` : ''] })] })) : null, m.spec.cpus ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Cpu, { size: 12 }), _jsx(SizableText, { size: "$1", color: "$ink", children: t('{n} cores', { n: m.spec.cpus }) })] })) : null, m.spec.memory ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(HardDrive, { size: 12 }), _jsx(SizableText, { size: "$1", color: "$ink", children: t('{size} RAM', { size: formatBytes(m.spec.memory) }) })] })) : null, m.spec.gpus && m.spec.gpus.length > 0 ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Zap, { size: 12, color: "$yellow10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: m.spec.gpus.map((g) => `${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`).join(', ') })] })) : null] })) : null, m.metrics && (m.metrics.cpuUtil || m.metrics.load1 || m.metrics.memUsed || m.metrics.gpuUtil) ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$3", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", children: [m.metrics.cpuUtil ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('CPU Utilization') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.cpuUtil), " ", m.metrics.cpuTemp ? `· ${m.metrics.cpuTemp}°C` : ''] })] })) : m.metrics.load1 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Load (1m, 5m, 15m)') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.load1, ", ", m.metrics.load5 ?? 0, ", ", m.metrics.load15 ?? 0] })] })) : null, m.metrics.memUsed ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Memory') }), _jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: m.spec?.memory ? `${formatBytes(m.metrics.memUsed)} / ${formatBytes(m.spec.memory)}` : t('{size} used', { size: formatBytes(m.metrics.memUsed) }) })] })) : null, m.metrics.gpuUtil !== undefined && m.metrics.gpuUtil > 0 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('GPU') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.gpuUtil), " ", m.metrics.gpuTemp ? `· ${m.metrics.gpuTemp}°C` : '', " ", m.metrics.gpuPower ? `· ${m.metrics.gpuPower}W` : ''] })] })) : null, m.metrics.model ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Serving Model') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.model, " ", m.metrics.decode ? `(${m.metrics.decode} tok/s)` : ''] })] })) : null] })) : null, _jsxs(XStack, { justify: "space-between", items: "center", flexWrap: "wrap", gap: "$2", pt: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: t('ID: {id}', { id: m.id }) }), m.seen ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Last heartbeat: {when}', { when: formatRelative(m.seen) }) })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Run directly on this machine:') }), _jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$0.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsxs(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: ["hanzo run --target ", m.id] }), _jsx(CopyButton, { value: `hanzo run --target ${m.id}`, label: t('Copy run command for {name}', { name: m.label }) })] })] })] })] })) : null] }, m.id));
142
+ }) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: told(list.error, t) }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
143
+ setNote(t('{name} is registered', { name: m.label }));
123
144
  list.reload();
124
- } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
145
+ } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: t('Rename machine'), name: renaming?.label ?? '',
125
146
  // Each dialog acts only while it is open, which is while its machine is chosen.
126
147
  run: async (label) => {
127
- await change(t, renaming.id, { label });
128
- setNote(`Renamed to ${label}`);
148
+ await change(target, renaming.id, { label });
149
+ setNote(t('Renamed to {name}', { name: label }));
129
150
  list.reload();
130
- } }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: `Remove ${removing?.label ?? 'this machine'}?`, says: "Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.", act: "Remove", run: async () => {
131
- await remove(t, removing.id);
132
- setNote(`${removing.label} is removed`);
151
+ } }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: removing ? t('Remove {name}?', { name: removing.label }) : t('Remove this machine?'), says: t('Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.'), act: t('Remove'), run: async () => {
152
+ await remove(target, removing.id);
153
+ setNote(t('{name} is removed', { name: removing.label }));
133
154
  list.reload();
134
- } }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
135
- const k = await key(t, keying.id);
155
+ } }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: keying ? t('Mint a claim key for {name}?', { name: keying.label }) : t('Mint a claim key for this machine?'), says: t('A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.'), act: t('Mint key'), run: async () => {
156
+ const k = await key(target, keying.id);
136
157
  setMinted({ machine: keying, key: k });
137
- } }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) }), _jsxs(YStack, { gap: "$1", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: { fontWeight: 600 }, children: "Start background worker with this key:" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2", py: "$1", rounded: "$2", bg: "$raised", children: [_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, children: ["export HANZO_TARGET_KEY=\"", minted.key, "\""] }), _jsx(CopyButton, { value: `export HANZO_TARGET_KEY="${minted.key}"\nhanzo code --serve --target ${minted.machine.id}`, label: "Copy key command" })] })] })] })) : null, _jsx(XStack, { justify: "flex-end", pt: "$2", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
158
+ } }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Claim key for {name}', { name: minted?.machine.label ?? '' }) }), minted ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Once, { value: minted.key, label: t('Claim key'), copy: t('Copy claim key'), children: _jsx(SizableText, { size: "$1", color: "$soft", children: t('A runner on this machine sends it as {header} when it claims the runs sent here, at {route}.', {
159
+ header: 'X-Target-Key',
160
+ route: `POST /v1/agent/targets/${minted.machine.id}/claim`,
161
+ }) }) }), _jsxs(YStack, { gap: "$1", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: { fontWeight: 600 }, children: t('Start background worker with this key:') }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2", py: "$1", rounded: "$2", bg: "$raised", children: [_jsxs(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: ["export HANZO_TARGET_KEY=\"", minted.key, "\""] }), _jsx(CopyButton, { value: `export HANZO_TARGET_KEY="${minted.key}"\nhanzo code --serve --target ${minted.machine.id}`, label: t('Copy key command') })] })] })] })) : null, _jsx(XStack, { justify: "flex-end", pt: "$2", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: t('Done') }) })] }) })] }));
138
162
  }
139
163
  /** Registering a machine by hand: its name, what it is, and the hostname its runs report. */
140
164
  function Adding({ open, onOpenChange, onAdded }) {
141
- const t = useTarget();
165
+ const { t } = useI18n();
166
+ const target = useTarget();
142
167
  const [label, setLabel] = useState('');
143
168
  const [kind, setKind] = useState('machine');
144
169
  const [hostname, setHostname] = useState('');
@@ -161,16 +186,16 @@ function Adding({ open, onOpenChange, onAdded }) {
161
186
  try {
162
187
  const targetLabel = label.trim() || hostname.trim();
163
188
  if (!targetLabel)
164
- throw new Error('A machine needs a name or a hostname');
165
- const m = await add(t, { label: targetLabel, kind, host: hostname, capacity: capacity.trim() || undefined });
189
+ throw new Error(phrase('A machine needs a name or a hostname'));
190
+ const m = await add(target, { label: targetLabel, kind, host: hostname, capacity: capacity.trim() || undefined });
166
191
  setWorking(false);
167
192
  onAdded(m);
168
193
  close();
169
194
  }
170
195
  catch (e) {
171
- setNote(e.message);
196
+ setNote(told(e, t));
172
197
  setWorking(false);
173
198
  }
174
199
  };
175
- return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Register a machine" }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", hint: "Left empty, it takes the hostname.", children: _jsx(Input, { value: label, onChangeText: setLabel, "aria-label": "Machine name", autoFocus: true }) }), _jsx(Field, { label: "Kind", children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": "Kind", children: KINDS.map((k) => (_jsxs(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": k, onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', items: "center", gap: "$1.5", children: [_jsx(KindIcon, { kind: k, size: 14 }), _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: k })] }, k))) }) }), _jsx(Field, { label: "Hostname", hint: "Optional. What its runs report. hanzo link, signed in as you on a machine with this hostname, takes this row over.", children: _jsx(Input, { value: hostname, onChangeText: setHostname, "aria-label": "Hostname", autoCapitalize: "none" }) }), _jsx(Field, { label: "Capacity / Hardware", hint: "Optional human summary (e.g. 16 vCPU / 64G / 1\u00D7 RTX 4090).", children: _jsx(Input, { value: capacity, onChangeText: setCapacity, "aria-label": "Capacity", placeholder: "e.g. 16 vCPU / 64G / 1\u00D7 RTX 4090" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Register" })] })] })] }) }));
200
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Register a machine') }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: t('Name'), hint: t('Left empty, it takes the hostname.'), children: _jsx(Input, { value: label, onChangeText: setLabel, "aria-label": t('Machine name'), autoFocus: true }) }), _jsx(Field, { label: t('Kind'), children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": t('Kind'), children: KINDS.map((k) => (_jsxs(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": kindOf(k, t), onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', items: "center", gap: "$1.5", children: [_jsx(KindIcon, { kind: k, size: 14 }), _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: kindOf(k, t) })] }, k))) }) }), _jsx(Field, { label: t('Hostname'), hint: t('Optional. What its runs report. {command}, signed in as you on a machine with this hostname, takes this row over.', { command: 'hanzo link' }), children: _jsx(Input, { value: hostname, onChangeText: setHostname, "aria-label": t('Hostname'), autoCapitalize: "none" }) }), _jsx(Field, { label: t('Capacity / Hardware'), hint: t('Optional human summary (e.g. {example}).', { example: '16 vCPU / 64G / 1× RTX 4090' }), children: _jsx(Input, { value: capacity, onChangeText: setCapacity, "aria-label": t('Capacity'), placeholder: t('e.g. {example}', { example: '16 vCPU / 64G / 1× RTX 4090' }) }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: t('Register') })] })] })] }) }));
176
201
  }
@@ -3,31 +3,39 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * Members: who is in the organization and in what role, and — for an org admin —
4
4
  * who has been invited, inviting someone by email, and withdrawing an invitation.
5
5
  * IAM holds both lists and decides who may read and write them.
6
+ *
7
+ * Its words are the reader's language (@hanzo/i18n `t`); a person's name,
8
+ * address and id, an invitation's code and IAM's own state are drawn as given.
6
9
  */
7
10
  import { SizableText, XStack, YStack } from '@hanzo/gui';
11
+ import { phrase } from '@hanzo/i18n';
12
+ import { useI18n } from '@hanzo/i18n/react';
8
13
  import { UserPlus } from '@hanzogui/lucide-icons-2';
9
14
  import { Button, Input } from '@hanzo/ui';
10
15
  import { useState } from 'react';
16
+ import { told } from '../api/call.js';
11
17
  import { EMAIL, invitations, invite, revoke, roster } from '../api/members.js';
12
18
  import { useRead } from '../data.js';
13
19
  import { useHost, useTarget } from '../host.js';
14
20
  import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
15
- const ROLE = { owner: 'Owner', admin: 'Admin', member: 'Member' };
21
+ /** A role by its name, English and marked (`phrase`), drawn through `t`. */
22
+ const ROLE = { owner: phrase('Owner'), admin: phrase('Admin'), member: phrase('Member') };
16
23
  function Initial({ name }) {
17
24
  return (_jsx(YStack, { width: 28, height: 28, rounded: 999, bg: "$raised", items: "center", justify: "center", shrink: 0, children: _jsx(SizableText, { size: "$2", color: "$ink", children: (name || '?').charAt(0).toUpperCase() }) }));
18
25
  }
19
26
  export function Members() {
27
+ const { t } = useI18n();
20
28
  const host = useHost();
21
- const t = useTarget();
29
+ const target = useTarget();
22
30
  const org = host.org ?? '';
23
31
  const signed = Boolean(host.person && org);
24
- const people = useRead(signed ? () => roster(t, org) : null, [], [t, org, signed]);
25
- const asked = useRead(signed && host.admin ? () => invitations(t, org) : null, [], [t, org, signed, host.admin]);
32
+ const people = useRead(signed ? () => roster(target, org) : null, [], [target, org, signed]);
33
+ const asked = useRead(signed && host.admin ? () => invitations(target, org) : null, [], [target, org, signed, host.admin]);
26
34
  const [email, setEmail] = useState('');
27
35
  const [working, setWorking] = useState(false);
28
36
  const [note, setNote] = useState('');
29
37
  if (!signed) {
30
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Members" }), _jsx(Soft, { children: host.person ? 'Choose an organization to see its members.' : 'Sign in to see this organization’s members.' })] }));
38
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Members') }), _jsx(Soft, { children: host.person ? t('Choose an organization to see its members.') : t('Sign in to see this organization’s members.') })] }));
31
39
  }
32
40
  const act = async (what, done, after) => {
33
41
  setWorking(true);
@@ -38,7 +46,7 @@ export function Members() {
38
46
  setNote(done);
39
47
  }
40
48
  catch (e) {
41
- setNote(e.message);
49
+ setNote(told(e, t));
42
50
  }
43
51
  finally {
44
52
  setWorking(false);
@@ -48,21 +56,22 @@ export function Members() {
48
56
  const address = email.trim();
49
57
  // Invite is offered only once something is typed.
50
58
  if (!EMAIL.test(address)) {
51
- setNote(`${address} is not an email address`);
59
+ setNote(t('{email} is not an email address', { email: address }));
52
60
  return;
53
61
  }
54
- void act(() => invite(t, org, address), `${address} is invited. IAM sends no email: share the code with them.`, () => {
62
+ void act(() => invite(target, org, address), t('{email} is invited. IAM sends no email: share the code with them.', { email: address }), () => {
55
63
  setEmail('');
56
64
  asked.reload();
57
65
  });
58
66
  };
59
67
  const open = asked.value.filter((i) => i.state === 'Active' && i.used < i.seats);
60
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Members", detail: `The people who can act in ${org}, and the role each one holds.` }), _jsx(Group, { title: "Members", detail: people.value.length ? `${people.value.length} in ${org}` : undefined, children: people.error ? (_jsx(Soft, { children: people.error.message })) : people.loading && !people.value.length ? (_jsx(Soft, { children: "Reading members\u2026" })) : people.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No one is on this organization\u2019s roster yet." }) })) : (_jsx(Card, { children: people.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Initial, { name: m.name }), title: m.name, detail: [m.user, m.since ? `since ${day(m.since)}` : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: m.role === 'member' ? '$soft' : '$ink', children: ROLE[m.role] }) }, m.user))) })) }), host.admin ? (_jsxs(Group, { title: "Invitations", detail: "An invitation is a one-seat code for one address, redeemed when that person signs up.", children: [_jsxs(Card, { children: [_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", flexWrap: "wrap", children: [_jsx(YStack, { flex: 1, minW: 200, children: _jsx(Input, { value: email, onChangeText: setEmail, placeholder: "name@company.com", "aria-label": "Email to invite", inputMode: "email", autoCapitalize: "none" }) }), _jsxs(Button, { size: "sm", disabled: working || !email.trim(), onPress: send, children: [_jsx(UserPlus, { size: 14 }), " Invite"] })] }), asked.error ? (_jsx(YStack, { borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Soft, { children: asked.error.message }) })) : null, asked.value.map((i) => (_jsx(Row, { title: i.email || i.name, mono: !i.email, detail: [
61
- i.code ? `Code ${i.code}` : '',
62
- `${i.used} of ${i.seats} joined`,
63
- i.state !== 'Active' ? i.state.toLowerCase() || 'not redeemable' : '',
64
- day(i.created) ? `invited ${day(i.created)}` : '',
68
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Members'), detail: t('The people who can act in {org}, and the role each one holds.', { org }) }), _jsx(Group, { title: t('Members'), detail: people.value.length ? t('{n} in {org}', { n: people.value.length, org }) : undefined, children: people.error ? (_jsx(Soft, { children: told(people.error, t) })) : people.loading && !people.value.length ? (_jsx(Soft, { children: t('Reading members…') })) : people.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: t('No one is on this organization’s roster yet.') }) })) : (_jsx(Card, { children: people.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Initial, { name: m.name }), title: m.name, detail: [m.user, m.since ? t('since {day}', { day: day(m.since) }) : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: m.role === 'member' ? '$soft' : '$ink', children: t(ROLE[m.role]) }) }, m.user))) })) }), host.admin ? (_jsxs(Group, { title: t('Invitations'), detail: t('An invitation is a one-seat code for one address, redeemed when that person signs up.'), children: [_jsxs(Card, { children: [_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", flexWrap: "wrap", children: [_jsx(YStack, { flex: 1, minW: 200, children: _jsx(Input, { value: email, onChangeText: setEmail, placeholder: "name@company.com", "aria-label": t('Email to invite'), inputMode: "email", autoCapitalize: "none" }) }), _jsxs(Button, { size: "sm", disabled: working || !email.trim(), onPress: send, children: [_jsx(UserPlus, { size: 14 }), " ", t('Invite')] })] }), asked.error ? (_jsx(YStack, { borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Soft, { children: told(asked.error, t) }) })) : null, asked.value.map((i) => (_jsx(Row, { title: i.email || i.name, mono: !i.email, detail: [
69
+ i.code ? t('Code {code}', { code: i.code }) : '',
70
+ t('{used} of {seats} joined', { used: i.used, seats: i.seats }),
71
+ // IAM's own word for a state, as given; none at all is ours.
72
+ i.state !== 'Active' ? i.state.toLowerCase() || t('not redeemable') : '',
73
+ day(i.created) ? t('invited {day}', { day: day(i.created) }) : '',
65
74
  ]
66
75
  .filter(Boolean)
67
- .join(' · '), trailing: _jsx(Button, { size: "sm", variant: "ghost", disabled: working, "aria-label": `Revoke the invitation for ${i.email || i.name}`, onPress: () => void act(() => revoke(t, i), 'The invitation is withdrawn', asked.reload), children: "Revoke" }) }, i.name)))] }), !asked.loading && !asked.error && asked.value.length > 0 && open.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Every invitation has been used or stopped." })) : null] })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin invites people and sees the open invitations." })), _jsx(Note, { children: note })] }));
76
+ .join(' · '), trailing: _jsx(Button, { size: "sm", variant: "ghost", disabled: working, "aria-label": t('Revoke the invitation for {who}', { who: i.email || i.name }), onPress: () => void act(() => revoke(target, i), t('The invitation is withdrawn'), asked.reload), children: t('Revoke') }) }, i.name)))] }), !asked.loading && !asked.error && asked.value.length > 0 && open.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Every invitation has been used or stopped.') })) : null] })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin invites people and sees the open invitations.') })), _jsx(Note, { children: note })] }));
68
77
  }