@hanzo/build 0.2.80 → 0.2.82

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 (168) 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/links.d.ts +1 -1
  29. package/lib/api/links.js +1 -1
  30. package/lib/api/machines.d.ts +0 -15
  31. package/lib/api/machines.js +6 -5
  32. package/lib/api/mcp.d.ts +0 -9
  33. package/lib/api/mcp.js +3 -2
  34. package/lib/api/members.d.ts +0 -15
  35. package/lib/api/members.js +4 -3
  36. package/lib/api/memory.d.ts +0 -11
  37. package/lib/api/memory.js +2 -1
  38. package/lib/api/places.d.ts +1 -9
  39. package/lib/api/places.js +12 -1
  40. package/lib/api/plugins.d.ts +1 -16
  41. package/lib/api/plugins.js +4 -3
  42. package/lib/api/profile.d.ts +0 -10
  43. package/lib/api/profile.js +5 -4
  44. package/lib/api/projects.d.ts +0 -14
  45. package/lib/api/projects.js +11 -10
  46. package/lib/api/provider.d.ts +2 -14
  47. package/lib/api/provider.js +12 -10
  48. package/lib/api/sandbox.d.ts +0 -17
  49. package/lib/api/sandbox.js +5 -4
  50. package/lib/api/sessions.d.ts +0 -17
  51. package/lib/api/sessions.js +5 -4
  52. package/lib/api/skills.d.ts +1 -15
  53. package/lib/api/skills.js +5 -4
  54. package/lib/api/studio.d.ts +0 -14
  55. package/lib/api/studio.js +6 -5
  56. package/lib/api/turn.d.ts +6 -28
  57. package/lib/api/turn.js +105 -49
  58. package/lib/api/verdict.d.ts +0 -9
  59. package/lib/api/verdict.js +2 -1
  60. package/lib/api/webhooks.d.ts +1 -14
  61. package/lib/api/webhooks.js +5 -4
  62. package/lib/ask.js +7 -4
  63. package/lib/automations.d.ts +5 -4
  64. package/lib/automations.js +95 -74
  65. package/lib/back.js +10 -4
  66. package/lib/builder.js +7 -5
  67. package/lib/carry.js +20 -16
  68. package/lib/customize/agents.js +78 -31
  69. package/lib/customize/connectors.js +62 -48
  70. package/lib/customize/index.js +43 -10
  71. package/lib/customize/plugins.js +34 -22
  72. package/lib/customize/say.d.ts +13 -1
  73. package/lib/customize/say.js +15 -21
  74. package/lib/customize/skills.js +31 -22
  75. package/lib/customize/ui.d.ts +5 -9
  76. package/lib/customize/ui.js +26 -16
  77. package/lib/data.d.ts +1 -1
  78. package/lib/data.js +3 -2
  79. package/lib/desk.js +45 -33
  80. package/lib/door.js +35 -10
  81. package/lib/environment.js +25 -19
  82. package/lib/files.js +43 -34
  83. package/lib/find.js +12 -9
  84. package/lib/foot.js +7 -4
  85. package/lib/forge.js +61 -40
  86. package/lib/gear.d.ts +2 -1
  87. package/lib/gear.js +6 -4
  88. package/lib/git.js +29 -19
  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/merge.d.ts +2 -2
  93. package/lib/meter.d.ts +1 -1
  94. package/lib/meter.js +14 -5
  95. package/lib/mic.js +12 -5
  96. package/lib/mind.d.ts +1 -0
  97. package/lib/mind.js +5 -3
  98. package/lib/nav.d.ts +6 -5
  99. package/lib/nav.js +37 -30
  100. package/lib/panel.d.ts +2 -1
  101. package/lib/panel.js +52 -36
  102. package/lib/plan.d.ts +4 -20
  103. package/lib/plan.js +26 -10
  104. package/lib/plans.js +49 -30
  105. package/lib/plugins/design.js +3 -2
  106. package/lib/plugins/index.d.ts +23 -4
  107. package/lib/plugins/index.js +11 -26
  108. package/lib/plugins/pdf.js +2 -1
  109. package/lib/plugins/project.js +7 -4
  110. package/lib/plugins/research.js +3 -2
  111. package/lib/plugins/sketch.js +6 -3
  112. package/lib/plugins/slides.js +3 -2
  113. package/lib/plugins/template.js +6 -3
  114. package/lib/plus.js +25 -13
  115. package/lib/prefs.d.ts +0 -14
  116. package/lib/prefs.js +2 -1
  117. package/lib/project.js +69 -62
  118. package/lib/prompt.d.ts +12 -11
  119. package/lib/prompt.js +38 -20
  120. package/lib/publish.js +17 -12
  121. package/lib/reply.js +6 -3
  122. package/lib/run.js +72 -54
  123. package/lib/say.d.ts +2 -0
  124. package/lib/say.js +7 -0
  125. package/lib/scheduled.js +54 -43
  126. package/lib/section.d.ts +1 -0
  127. package/lib/section.js +8 -2
  128. package/lib/settings/account.d.ts +1 -0
  129. package/lib/settings/account.js +31 -21
  130. package/lib/settings/billing.js +55 -28
  131. package/lib/settings/capabilities.js +26 -15
  132. package/lib/settings/card.js +19 -12
  133. package/lib/settings/code.js +22 -11
  134. package/lib/settings/credits.d.ts +4 -4
  135. package/lib/settings/credits.js +34 -21
  136. package/lib/settings/environments.js +21 -14
  137. package/lib/settings/general.js +26 -14
  138. package/lib/settings/index.js +6 -3
  139. package/lib/settings/integrations.js +42 -28
  140. package/lib/settings/keys.js +37 -19
  141. package/lib/settings/list.d.ts +0 -5
  142. package/lib/settings/list.js +24 -15
  143. package/lib/settings/machines.d.ts +3 -0
  144. package/lib/settings/machines.js +63 -38
  145. package/lib/settings/members.js +23 -14
  146. package/lib/settings/memory.js +12 -6
  147. package/lib/settings/notifications.js +32 -19
  148. package/lib/settings/plan.d.ts +1 -1
  149. package/lib/settings/plan.js +40 -17
  150. package/lib/settings/privacy.js +26 -13
  151. package/lib/settings/ui.d.ts +3 -2
  152. package/lib/settings/ui.js +9 -3
  153. package/lib/settings/usage.js +7 -3
  154. package/lib/shelf.js +20 -17
  155. package/lib/side.d.ts +1 -26
  156. package/lib/side.js +11 -5
  157. package/lib/standing.d.ts +2 -10
  158. package/lib/standing.js +5 -3
  159. package/lib/suggest.d.ts +8 -28
  160. package/lib/suggest.js +39 -4
  161. package/lib/suggestions.js +14 -5
  162. package/lib/summary.d.ts +1 -1
  163. package/lib/summary.js +4 -1
  164. package/lib/sync.js +22 -12
  165. package/lib/tools.d.ts +4 -3
  166. package/lib/tools.js +10 -7
  167. package/lib/transcript.js +10 -8
  168. package/package.json +4 -2
package/lib/landing.js CHANGED
@@ -12,8 +12,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  * names one, and an ask with none chosen opens the picker rather than making a
13
13
  * project up from its words. The choices a person made here are kept in this
14
14
  * browser, per org, so the next New starts where the last one did.
15
+ *
16
+ * Its words are the reader's language (@hanzo/i18n `t`, the target is
17
+ * `target`); a codebase's, a machine's and a branch's names are drawn as given.
15
18
  */
16
19
  import { SizableText, XStack, YStack } from '@hanzo/gui';
20
+ import { phrase } from '@hanzo/i18n';
21
+ import { useI18n } from '@hanzo/i18n/react';
17
22
  import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
18
23
  import { Button } from '@hanzo/ui';
19
24
  import { BranchSelect, ChipSelect, FooterLink, RepoSelect } from '@hanzo/ui/product';
@@ -43,10 +48,10 @@ import { MEASURE, Prompt, useEar } from './prompt.js';
43
48
  import { Monitor as Device } from '@hanzogui/lucide-icons-2';
44
49
  import { compose, Files, pick, read as readFiles } from './tools.js';
45
50
  import { useSpoken } from './voice.js';
46
- /** One vocabulary for the mode, on New and in a workspace. */
51
+ /** One vocabulary for the mode, on New and in a workspace: English, marked (`phrase`), drawn through `t`. */
47
52
  export const MODES = [
48
- { id: 'build', label: 'Build', hint: 'Edits, commits and pushes a branch' },
49
- { id: 'plan', label: 'Plan', hint: 'Plans the change and writes nothing' },
53
+ { id: 'build', label: phrase('Build'), hint: phrase('Edits, commits and pushes a branch') },
54
+ { id: 'plan', label: phrase('Plan'), hint: phrase('Plans the change and writes nothing') },
50
55
  ];
51
56
  /** The org's repositories before they have been read: no answer, so nothing is judged from it. */
52
57
  const NOHOMES = { homes: [], whole: false };
@@ -79,7 +84,8 @@ export function hold(u) {
79
84
  }
80
85
  export function Landing({ onStarted }) {
81
86
  const host = useHost();
82
- const t = useTarget();
87
+ const target = useTarget();
88
+ const { t } = useI18n();
83
89
  const signed = Boolean(host.person);
84
90
  const { prefs, save: savePrefs } = usePrefs();
85
91
  const [stored, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
@@ -109,21 +115,21 @@ export function Landing({ onStarted }) {
109
115
  // The handover is read on this mount only.
110
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
111
117
  }, []);
112
- const places = usePlaces(t, signed);
118
+ const places = usePlaces(target, signed);
113
119
  // The model and effort Chat shares, and dictation into the ask.
114
- const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
115
- const mind = useMind(t, limits?.plan);
116
- const voice = useVoice({ speech: useEar(t), language: useSpoken(), ...useDictation(draft, setDraft) });
117
- const talk = useTalk({ baseUrl: t.api, token: () => t.token(), ...(t.org ? { org: t.org } : {}) });
120
+ const { limits } = useLimits(signed ? (signal) => readLimits(target, signal) : null, target);
121
+ const mind = useMind(target, limits?.plan);
122
+ const voice = useVoice({ speech: useEar(target), language: useSpoken(), ...useDictation(draft, setDraft) });
123
+ const talk = useTalk({ baseUrl: target.api, token: () => target.token(), ...(target.org ? { org: target.org } : {}) });
118
124
  // What the run carries beside the ask: skills, memory and the mode, as Chat's turns do.
119
- const carry = useCarry(t, '', (tab) => host.go(path({ kind: 'customize', tab })), {
125
+ const carry = useCarry(target, '', (tab) => host.go(path({ kind: 'customize', tab })), {
120
126
  draft,
121
127
  setDraft,
122
128
  // A run reads text: a file that is one rides the ask, a picture goes with a chat.
123
129
  attach: (list) => {
124
130
  const text = list.filter((f) => !f.type.startsWith('image/'));
125
131
  if (text.length < list.length)
126
- setNote('A run reads text files; send a picture in Chat.');
132
+ setNote(t('A run reads text files; send a picture in Chat.'));
127
133
  if (text.length) {
128
134
  const dt = new DataTransfer();
129
135
  for (const f of text)
@@ -138,16 +144,16 @@ export function Landing({ onStarted }) {
138
144
  go: (href) => (href.startsWith('http') ? host.open(href) : host.go(href)),
139
145
  }, DEV_MODES);
140
146
  const mode = carry.mode.value === 'plan' ? 'plan' : 'build';
141
- const hints = useSuggestions(t);
147
+ const hints = useSuggestions(target);
142
148
  // The chosen codebase's environment, so New can say when it has none yet.
143
149
  const codebase = isForge(kept.repo) ? kept.repo.name : '';
144
150
  // The org's repositories on the forge: what a run and an environment can name.
145
- const homes = useCached(signed ? `${host.org}:homes` : null, (signal) => readHomes(t, signal), NOHOMES);
151
+ const homes = useCached(signed ? `${host.org}:homes` : null, (signal) => readHomes(target, signal), NOHOMES);
146
152
  // A kept codebase the org's whole list does not hold — a board's key once handed over as one — is not offered as one.
147
153
  const stranger = codebase !== '' && homes.value.whole && !homes.value.homes.some((h) => h.name.toLowerCase() === codebase.toLowerCase());
148
154
  // Read once the list has answered (or failed), so a name that is not the org's is never asked for.
149
155
  const settled = homes.value !== NOHOMES || homes.error !== null;
150
- const found = useRead(signed && codebase && settled && !stranger ? () => read(t, codebase) : null, null, [t, signed, codebase, settled, stranger]);
156
+ const found = useRead(signed && codebase && settled && !stranger ? () => read(target, codebase) : null, null, [target, signed, codebase, settled, stranger]);
151
157
  // A read kept from the codebase before is not this one's.
152
158
  const env = { ...found, value: found.value && found.value.repo === codebase && !stranger ? found.value : null };
153
159
  const place = places.value.find((p) => p.id === kept.place) ?? SANDBOX;
@@ -158,7 +164,7 @@ export function Landing({ onStarted }) {
158
164
  // pushes and proposes on GitHub, where its people work. A GitHub read that
159
165
  // fails leaves the forge's list standing.
160
166
  const loadRepos = useCallback(async (q) => {
161
- const [page, hub] = await Promise.all([codebases(t, q), githubRepos(t, { q }).catch(() => null)]);
167
+ const [page, hub] = await Promise.all([codebases(target, q), githubRepos(target, { q }).catch(() => null)]);
162
168
  const repos = page.map(asRepo);
163
169
  for (const r of repos)
164
170
  known.current.set(r.name, r);
@@ -166,15 +172,15 @@ export function Landing({ onStarted }) {
166
172
  for (const r of github)
167
173
  hubs.current.set(r.full_name.toLowerCase(), r);
168
174
  return { repos: [...repos, ...github], next: null };
169
- }, [t]);
175
+ }, [target]);
170
176
  // Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
171
177
  const home = kept.repo?.default_branch || 'main';
172
178
  const loadBranches = useCallback(async (q) => {
173
- const got = await one(t, codebase);
179
+ const got = await one(target, codebase);
174
180
  const needle = q.trim().toLowerCase();
175
181
  const names = (got?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
176
182
  return { branches: names.map((name) => ({ name })), next: null };
177
- }, [t, codebase, home]);
183
+ }, [target, codebase, home]);
178
184
  // The composer sends only a draft with words in it, and not while a send is out.
179
185
  const send = async (said = draft) => {
180
186
  if (!signed) {
@@ -185,18 +191,18 @@ export function Landing({ onStarted }) {
185
191
  const repo = isForge(kept.repo) ? kept.repo : null;
186
192
  const hub = isHub(kept.repo) ? kept.repo : null;
187
193
  if (!repo && !hub) {
188
- setNote('Choose the project this run works on: a repository of this organization, or one on its GitHub.');
194
+ setNote(t('Choose the project this run works on: a repository of this organization, or one on its GitHub.'));
189
195
  setPicking(true);
190
196
  return;
191
197
  }
192
198
  // A plan or a setup routed to a machine is refused here, in its own words, before it leaves the page.
193
- const why = unhonoured(mode, place.id);
199
+ const why = unhonoured(mode, place.id, t);
194
200
  if (why) {
195
201
  setNote(why);
196
202
  return;
197
203
  }
198
204
  if (stranger) {
199
- setNote(`${codebase} is not one of this organization’s repositories. Choose one, or leave it empty to start a new project.`);
205
+ setNote(t('{repo} is not one of this organization’s repositories. Choose one, or leave it empty to start a new project.', { repo: codebase }));
200
206
  setPicking(true);
201
207
  return;
202
208
  }
@@ -212,11 +218,11 @@ export function Landing({ onStarted }) {
212
218
  return;
213
219
  }
214
220
  // Manual: the run is asked for before it starts; it edits, commits and pushes on its own once it has.
215
- if (!(await carry.allow({ id: 'run', name: 'Dev to start this run', note: 'It edits, tests, commits and pushes its own branch' }))) {
216
- setNote('Not started.');
221
+ if (!(await carry.allow({ id: 'run', name: t('Dev to start this run'), note: t('It edits, tests, commits and pushes its own branch') }))) {
222
+ setNote(t('Not started.'));
217
223
  return;
218
224
  }
219
- const run = await start(t, {
225
+ const run = await start(target, {
220
226
  prompt: compose([said.trim(), carry.text, prepped.system ?? ''].filter(Boolean).join('\n\n---\n'), files),
221
227
  // The name alone. The org rides the request, and a slash is not a repo name.
222
228
  // An empty base or place is not sent: the default branch, and the sandbox.
@@ -234,7 +240,7 @@ export function Landing({ onStarted }) {
234
240
  onStarted(run.session);
235
241
  }
236
242
  catch (e) {
237
- setNote(plain(e, repo?.name ?? hub?.full_name ?? 'a new project', 'run'));
243
+ setNote(plain(e, repo?.name ?? hub?.full_name ?? t('a new project'), 'run', t));
238
244
  }
239
245
  finally {
240
246
  setBusy(false);
@@ -285,13 +291,13 @@ export function Landing({ onStarted }) {
285
291
  return;
286
292
  case 'schedule':
287
293
  setNote('');
288
- void book(t, a, 'dev').then((made) => host.go(path({ kind: 'automation', id: made.id })), (e) => setNote(e instanceof Error && e.message ? e.message : 'The scheduled task could not be made.'));
294
+ void book(target, a, 'dev').then((made) => host.go(path({ kind: 'automation', id: made.id })), (e) => setNote(e instanceof Error && e.message ? e.message : t('The scheduled task could not be made.')));
289
295
  return;
290
296
  }
291
297
  };
292
298
  /** Start the agent that finds a codebase's environment, and open it: its run is the progress. A refusal is the dialog's to say. */
293
299
  const setup = async (repo) => {
294
- const run = await start(t, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: mind.effort });
300
+ const run = await start(target, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: mind.effort });
295
301
  onStarted(run.session);
296
302
  };
297
303
  /** The codebase a dialog chose, chosen here too. */
@@ -303,22 +309,24 @@ export function Landing({ onStarted }) {
303
309
  // Cloud is the sandbox, set up by the chosen codebase's environment.
304
310
  const setUp = codebase && env.value
305
311
  ? env.value.state === 'ready'
306
- ? `${codebase} environment`
312
+ ? t('{repo} environment', { repo: codebase })
307
313
  : env.value.state === 'proposed'
308
- ? `${codebase} environment proposed`
314
+ ? t('{repo} environment proposed', { repo: codebase })
309
315
  : unset(env.value)
310
- ? `no ${codebase} environment yet`
311
- : `${codebase} environment, empty`
316
+ ? t('no {repo} environment yet', { repo: codebase })
317
+ : t('{repo} environment, empty', { repo: codebase })
312
318
  : '';
313
319
  const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
314
320
  id: p.id || 'sandbox',
315
- label: p.id ? p.label : 'Cloud',
316
- hint: p.id ? ['Remote control', p.status, p.capacity].filter(Boolean).join(' · ') : ['Hanzo sandbox', setUp].filter(Boolean).join(' · '),
321
+ label: p.id ? p.label : t('Cloud'),
322
+ hint: p.id ? [t('Remote control'), p.status, p.capacity].filter(Boolean).join(' · ') : [t('Hanzo sandbox'), setUp].filter(Boolean).join(' · '),
317
323
  disabled: !ready(p),
318
324
  place: p,
319
- })), [places.value, setUp]);
325
+ })), [places.value, setUp, t]);
320
326
  const branch = kept.branch || kept.repo?.default_branch || 'main';
321
- const where = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) || isHub(kept.repo) ? kept.repo : null, onChange: (r) => {
327
+ const where = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: t('Where the run runs'), icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : t('Cloud'), chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: t('Search machines…'), loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: t('Set up remote control') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Run {command} on a machine: it joins this list, and a run there uses its checkout and credentials.', {
328
+ command: (_jsx(SizableText, { size: "$1", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo link" })),
329
+ }) })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) || isHub(kept.repo) ? kept.repo : null, onChange: (r) => {
322
330
  const hub = r.github ? hubs.current.get((r.full_name ?? "").toLowerCase()) : undefined;
323
331
  setIssue(0);
324
332
  if (hub) {
@@ -328,20 +336,20 @@ export function Landing({ onStarted }) {
328
336
  const row = known.current.get(r.name) ?? chosen(r);
329
337
  set({ repo: row, branch: row.default_branch });
330
338
  }, load: loadRepos, troubleshoot: {
331
- label: 'Missing a GitHub repository? Bring it onto the forge',
339
+ label: t('Missing a GitHub repository? Bring it onto the forge'),
332
340
  onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })),
333
- }, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label: "Refresh list", onPress: () => setRound((n) => n + 1) }) }), note: "Repositories on the forge and on your organization's GitHub. Type to search.", action: {
334
- label: 'Add to project',
341
+ }, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label: t('Refresh list'), onPress: () => setRound((n) => n + 1) }) }), note: t("Repositories on the forge and on your organization's GitHub. Type to search."), action: {
342
+ label: t('Add to project'),
335
343
  onPress: (r) => {
336
344
  const row = known.current.get(r.name);
337
345
  setAdding({
338
- repo: row?.clone || `${t.api}/v1/git/${r.owner}/${r.name}.git`,
346
+ repo: row?.clone || `${target.api}/v1/git/${r.owner}/${r.name}.git`,
339
347
  title: row?.full_name || `${r.owner}/${r.name}`,
340
348
  ref: r.default_branch || 'main',
341
349
  name: r.name,
342
350
  });
343
351
  },
344
- }, placeholder: "Choose a project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null] }));
352
+ }, placeholder: t('Choose a project'), disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null] }));
345
353
  const head = files.length ? (_jsx(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: _jsx(Files, { files: files, onFiles: setFiles }) })) : undefined;
346
354
  // The machines a run can go to, as the `+` menu's Devices: the same choice as `where`'s.
347
355
  const devices = {
@@ -350,10 +358,10 @@ export function Landing({ onStarted }) {
350
358
  loading: places.loading,
351
359
  error: places.error?.message ?? null,
352
360
  };
353
- return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$3", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: "Build what\u2019s next." }), _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: `${codebase} is not one of this organization’s repositories.` }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: "Choose a repository" })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
354
- ? `A proposed environment for ${codebase} is waiting for review.`
355
- : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(mode, place.id) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: "Describe a task or ask a question", head: head, plus: {
356
- upload: () => pick(files, setFiles, setNote),
361
+ return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$3", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: t('Build what’s next.') }), _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: t('{repo} is not one of this organization’s repositories.', { repo: codebase }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: t('Choose a repository') })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
362
+ ? t('A proposed environment for {repo} is waiting for review.', { repo: codebase })
363
+ : t('{repo} has no environment yet, so every run starts it bare.', { repo: codebase }) }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: t('Review') })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: t('Set up environment') }))] })) : null, note || unhonoured(mode, place.id, t) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id, t) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: t('Describe a task or ask a question'), head: head, plus: {
364
+ upload: () => pick(files, setFiles, setNote, t),
357
365
  skills: carry.skills,
358
366
  design: carry.design,
359
367
  plugins: carry.plugins,
package/lib/library.js CHANGED
@@ -11,13 +11,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  * with the org, copy its link, rename, move to a project, use in Studio or in
12
12
  * Chat, and delete. What a colleague shared is drawn with their name and offers
13
13
  * only what a reader may do with it.
14
+ *
15
+ * Its words are the reader's language (@hanzo/i18n `t`); an item's name, a
16
+ * colleague's, a project's and what an item holds are drawn as given.
14
17
  */
15
18
  import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
16
19
  import { AudioLines, Box, Code2, Copy, Download, ExternalLink, FileText, FolderInput, Image as Picture, LayoutGrid, List, MessageSquare, MoreHorizontal, Pencil, Presentation, Search, Share2, Sparkles, Trash2, Type, Video, } from '@hanzogui/lucide-icons-2';
20
+ import { phrase } from '@hanzo/i18n';
21
+ import { useI18n } from '@hanzo/i18n/react';
17
22
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
18
23
  import { ChipSelect } from '@hanzo/ui/product';
19
24
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
20
25
  import { ago } from './ago.js';
26
+ import { told } from './api/call.js';
21
27
  import { change, days, get, items as list, KINDS, remove, size, SOURCE } from './api/library.js';
22
28
  import { Confirm, Rename } from './ask.js';
23
29
  import { useKept, useProjects, useRead } from './data.js';
@@ -28,18 +34,22 @@ import { Panel } from './panel.js';
28
34
  import { gap, pane } from './pane.js';
29
35
  import { Sites } from './shelf.js';
30
36
  import { useDeck } from './tabs.js';
37
+ /** Where an item was made: Chat, Dev and Studio by their names, the rest in words, marked (`phrase`) and drawn through `t`. */
31
38
  const FROM = [
32
- { id: '', label: 'Anywhere' },
39
+ { id: '', label: phrase('Anywhere') },
33
40
  { id: 'chat', label: 'Chat' },
34
41
  { id: 'dev', label: 'Dev' },
35
42
  { id: 'studio', label: 'Studio' },
36
- { id: 'workflow', label: 'Workflows' },
43
+ { id: 'workflow', label: phrase('Workflows') },
37
44
  ];
38
45
  const WHOSE = [
39
- { id: 'all', label: 'Mine and shared' },
40
- { id: 'mine', label: 'Mine' },
41
- { id: 'shared', label: 'Shared with me' },
46
+ { id: 'all', label: phrase('Mine and shared') },
47
+ { id: 'mine', label: phrase('Mine') },
48
+ { id: 'shared', label: phrase('Shared with me') },
42
49
  ];
50
+ /** A day's list, named: the days the Library says in words each a sentence, a month as given. */
51
+ const MADE = { Today: phrase('Made today'), Yesterday: phrase('Made yesterday'), Earlier: phrase('Made earlier') };
52
+ const made = (label, t) => (MADE[label] ? t(MADE[label]) : t('Made {month}', { month: label.toLowerCase() }));
43
53
  /** How many items a page reads. */
44
54
  const PAGE = 60;
45
55
  /** The side panel's scope: the items a reader opened, kept as they left them. */
@@ -77,8 +87,8 @@ export function Thumb({ x, size: n }) {
77
87
  return (_jsx(YStack, { width: n ?? '100%', height: n, style: n ? undefined : { aspectRatio: '1 / 1' }, rounded: "$3", overflow: "hidden", bg: "$raised", items: "center", justify: "center", shrink: 0, "aria-hidden": true, children: src && !broken ? (_jsx("img", { src: src, alt: "", loading: "lazy", onError: () => setBroken(true), style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } })) : (_jsx(YStack, { opacity: 0.7, children: glyph(x.kind, n ? Math.max(14, Math.round(n / 2.6)) : 28) })) }));
78
88
  }
79
89
  /** What a tile says under its name: where it came from, whose, and when. */
80
- function meta(x) {
81
- return [SOURCE[x.source], x.mine ? '' : `shared by ${x.owner}`, ago(x.created)].filter(Boolean).join(' · ');
90
+ function meta(x, t) {
91
+ return [t(SOURCE[x.source]), x.mine ? '' : t('shared by {owner}', { owner: x.owner }), ago(x.created)].filter(Boolean).join(' · ');
82
92
  }
83
93
  /** An item's link, to paste: this page with the item named. */
84
94
  function linkTo(x) {
@@ -117,8 +127,9 @@ function named() {
117
127
  }
118
128
  /** What can be done with one item, as menu rows. */
119
129
  function useActs(onChanged, onGone) {
130
+ const { t } = useI18n();
120
131
  const host = useHost();
121
- const t = useTarget();
132
+ const target = useTarget();
122
133
  const [renaming, setRenaming] = useState(null);
123
134
  const [deleting, setDeleting] = useState(null);
124
135
  const [moving, setMoving] = useState(null);
@@ -129,7 +140,7 @@ function useActs(onChanged, onGone) {
129
140
  await work();
130
141
  }
131
142
  catch (e) {
132
- setNote(e.message);
143
+ setNote(told(e, t));
133
144
  }
134
145
  };
135
146
  const copy = (text, said) => act(async () => {
@@ -137,51 +148,52 @@ function useActs(onChanged, onGone) {
137
148
  setNote(said);
138
149
  });
139
150
  const rows = (x) => [
140
- ...(x.origin ? [{ key: 'open', label: 'Open where it was made', icon: _jsx(ExternalLink, { size: 16 }), onSelect: () => host.open(x.origin) }] : []),
141
- ...(x.download ? [{ key: 'download', label: 'Download', icon: _jsx(Download, { size: 16 }), onSelect: () => window.open(x.download, '_blank', 'noopener,noreferrer') }] : []),
151
+ ...(x.origin ? [{ key: 'open', label: t('Open where it was made'), icon: _jsx(ExternalLink, { size: 16 }), onSelect: () => host.open(x.origin) }] : []),
152
+ ...(x.download ? [{ key: 'download', label: t('Download'), icon: _jsx(Download, { size: 16 }), onSelect: () => window.open(x.download, '_blank', 'noopener,noreferrer') }] : []),
142
153
  ...(x.mine
143
154
  ? [
144
155
  {
145
156
  key: 'share',
146
- label: x.shared ? 'Stop sharing' : 'Share with organization',
157
+ label: x.shared ? t('Stop sharing') : t('Share with organization'),
147
158
  icon: _jsx(Share2, { size: 16 }),
148
- description: x.shared ? 'Only you see it' : 'Everyone in it can open it',
159
+ description: x.shared ? t('Only you see it') : t('Everyone in it can open it'),
149
160
  onSelect: () => void act(async () => {
150
- const next = await change(t, x.id, { shared: !x.shared });
161
+ const next = await change(target, x.id, { shared: !x.shared });
151
162
  onChanged(next);
152
- setNote(next.shared ? `${next.name} is shared with the organization` : `${next.name} is yours alone`);
163
+ setNote(next.shared ? t('{name} is shared with the organization', { name: next.name }) : t('{name} is yours alone', { name: next.name }));
153
164
  }),
154
165
  },
155
166
  ]
156
167
  : []),
157
- { key: 'link', label: 'Copy link', icon: _jsx(Copy, { size: 16 }), onSelect: () => void copy(linkTo(x), 'Link copied') },
168
+ { key: 'link', label: t('Copy link'), icon: _jsx(Copy, { size: 16 }), onSelect: () => void copy(linkTo(x), t('Link copied')) },
158
169
  ...(x.mine
159
170
  ? [
160
- { key: 'rename', label: 'Rename', icon: _jsx(Pencil, { size: 16 }), onSelect: () => setRenaming(x) },
161
- { key: 'move', label: 'Move to project', icon: _jsx(FolderInput, { size: 16 }), onSelect: () => setMoving(x) },
171
+ { key: 'rename', label: t('Rename'), icon: _jsx(Pencil, { size: 16 }), onSelect: () => setRenaming(x) },
172
+ { key: 'move', label: t('Move to project'), icon: _jsx(FolderInput, { size: 16 }), onSelect: () => setMoving(x) },
162
173
  ]
163
174
  : []),
164
- { key: 'studio', label: 'Use in Studio', icon: _jsx(Sparkles, { size: 16 }), onSelect: () => host.open(`/studio?asset=${encodeURIComponent(x.id)}`) },
165
- { key: 'chat', label: 'Use in Chat', icon: _jsx(MessageSquare, { size: 16 }), onSelect: () => host.open(`/chat?attach=${encodeURIComponent(x.id)}`) },
166
- ...(x.mine ? [{ key: 'delete', label: 'Delete', icon: _jsx(Trash2, { size: 16 }), onSelect: () => setDeleting(x) }] : []),
175
+ { key: 'studio', label: t('Use in Studio'), icon: _jsx(Sparkles, { size: 16 }), onSelect: () => host.open(`/studio?asset=${encodeURIComponent(x.id)}`) },
176
+ { key: 'chat', label: t('Use in Chat'), icon: _jsx(MessageSquare, { size: 16 }), onSelect: () => host.open(`/chat?attach=${encodeURIComponent(x.id)}`) },
177
+ ...(x.mine ? [{ key: 'delete', label: t('Delete'), icon: _jsx(Trash2, { size: 16 }), onSelect: () => setDeleting(x) }] : []),
167
178
  ];
168
- const dialogs = (_jsxs(_Fragment, { children: [_jsx(Rename, { open: Boolean(renaming), onOpenChange: (o) => !o && setRenaming(null), title: "Rename", name: renaming?.name ?? '', run: async (name) => {
169
- const next = await change(t, renaming.id, { name });
179
+ const dialogs = (_jsxs(_Fragment, { children: [_jsx(Rename, { open: Boolean(renaming), onOpenChange: (o) => !o && setRenaming(null), title: t('Rename'), name: renaming?.name ?? '', run: async (name) => {
180
+ const next = await change(target, renaming.id, { name });
170
181
  onChanged(next);
171
- } }), _jsx(Confirm, { open: Boolean(deleting), onOpenChange: (o) => !o && setDeleting(null), title: `Delete ${deleting?.name ?? ''}?`, says: "It leaves the Library and its file is deleted. Anyone it was shared with loses it too.", act: "Delete", run: async () => {
172
- await remove(t, deleting.id);
182
+ } }), _jsx(Confirm, { open: Boolean(deleting), onOpenChange: (o) => !o && setDeleting(null), title: t('Delete {name}?', { name: deleting?.name ?? '' }), says: t('It leaves the Library and its file is deleted. Anyone it was shared with loses it too.'), act: t('Delete'), run: async () => {
183
+ await remove(target, deleting.id);
173
184
  onGone(deleting.id);
174
185
  } }), _jsx(Move, { item: moving, onClose: () => setMoving(null), onMoved: (next) => {
175
186
  onChanged(next);
176
- setNote(next.project ? `${next.name} is in ${next.project}` : `${next.name} is in no project`);
187
+ setNote(next.project ? t('{name} is in {project}', { name: next.name, project: next.project }) : t('{name} is in no project', { name: next.name }));
177
188
  } })] }));
178
189
  return { rows, dialogs, note };
179
190
  }
180
191
  /** Choosing the project an item belongs to, or none. */
181
192
  function Move({ item: x, onClose, onMoved }) {
193
+ const { t } = useI18n();
182
194
  const host = useHost();
183
- const t = useTarget();
184
- const projects = useProjects(t, Boolean(host.person) && Boolean(x));
195
+ const target = useTarget();
196
+ const projects = useProjects(target, Boolean(host.person) && Boolean(x));
185
197
  const [busy, setBusy] = useState(false);
186
198
  const [note, setNote] = useState('');
187
199
  const pick = async (project) => {
@@ -190,29 +202,32 @@ function Move({ item: x, onClose, onMoved }) {
190
202
  setBusy(true);
191
203
  setNote('');
192
204
  try {
193
- onMoved(await change(t, x.id, { project }));
205
+ onMoved(await change(target, x.id, { project }));
194
206
  onClose();
195
207
  }
196
208
  catch (e) {
197
- setNote(e.message);
209
+ setNote(told(e, t));
198
210
  }
199
211
  finally {
200
212
  setBusy(false);
201
213
  }
202
214
  };
203
- return (_jsx(Dialog, { open: Boolean(x), onOpenChange: (o) => !o && !busy && onClose(), children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Move to project" }), _jsx(YStack, { gap: "$1", maxH: 320, overflow: "scroll", children: projects.loading && !projects.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading projects\u2026" })) : projects.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: projects.error.message })) : ([{ slug: '', name: 'No project' }, ...projects.value].map((p) => (_jsx(XStack, { render: "button", "aria-label": p.slug ? `Move to ${p.name || p.slug}` : 'Move to no project', "aria-current": x?.project === p.slug ? 'true' : undefined, disabled: busy, onPress: () => void pick(p.slug), px: "$3", py: "$2", rounded: "$3", bg: x?.project === p.slug ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(SizableText, { size: "$2", color: "$ink", children: p.name || p.slug }) }, p.slug || 'none')))) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }) }));
215
+ return (_jsx(Dialog, { open: Boolean(x), onOpenChange: (o) => !o && !busy && onClose(), children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: t('Move to project') }), _jsx(YStack, { gap: "$1", maxH: 320, overflow: "scroll", children: projects.loading && !projects.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: t('Reading projects…') })) : projects.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: told(projects.error, t) })) : ([{ slug: '', name: t('No project') }, ...projects.value].map((p) => (_jsx(XStack, { render: "button", "aria-label": p.slug ? t('Move to {project}', { project: p.name || p.slug }) : t('Move to no project'), "aria-current": x?.project === p.slug ? 'true' : undefined, disabled: busy, onPress: () => void pick(p.slug), px: "$3", py: "$2", rounded: "$3", bg: x?.project === p.slug ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(SizableText, { size: "$2", color: "$ink", children: p.name || p.slug }) }, p.slug || 'none')))) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }) }));
204
216
  }
205
217
  /** One item's menu, as a quiet control. */
206
218
  function Acts({ x, rows }) {
207
- return (_jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${x.name}`, width: 30, height: 30, items: "center", justify: "center", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", shrink: 0, children: _jsx(MoreHorizontal, { size: 16 }) }), items: rows }));
219
+ const { t } = useI18n();
220
+ return (_jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": t('Actions for {name}', { name: x.name }), width: 30, height: 30, items: "center", justify: "center", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", shrink: 0, children: _jsx(MoreHorizontal, { size: 16 }) }), items: rows }));
208
221
  }
209
222
  /** One tile of the grid: the still, then its name and where it came from; its menu beside the name. */
210
223
  function Tile({ x, onOpen, rows }) {
211
- return (_jsxs(YStack, { role: "listitem", gap: "$2", minW: 0, children: [_jsx(YStack, { render: "button", "aria-label": `Open ${x.name}`, onPress: onOpen, rounded: "$4", overflow: "hidden", cursor: "pointer", hoverStyle: { opacity: 0.88 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Thumb, { x: x }) }), _jsxs(XStack, { items: "flex-start", gap: "$1", minW: 0, children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta(x) })] }), _jsx(Acts, { x: x, rows: rows })] })] }));
224
+ const { t } = useI18n();
225
+ return (_jsxs(YStack, { role: "listitem", gap: "$2", minW: 0, children: [_jsx(YStack, { render: "button", "aria-label": t('Open {name}', { name: x.name }), onPress: onOpen, rounded: "$4", overflow: "hidden", cursor: "pointer", hoverStyle: { opacity: 0.88 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Thumb, { x: x }) }), _jsxs(XStack, { items: "flex-start", gap: "$1", minW: 0, children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta(x, t) })] }), _jsx(Acts, { x: x, rows: rows })] })] }));
212
226
  }
213
227
  /** One row of the list: a small still, its name, where it came from, how large, and its menu. */
214
228
  function Row({ x, onOpen, rows }) {
215
- return (_jsxs(XStack, { role: "listitem", items: "center", gap: "$3", px: "$2", py: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, minW: 0, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${x.name}`, onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", cursor: "pointer", style: { textAlign: 'left' }, children: [_jsx(Thumb, { x: x, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta(x) })] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 72, style: { textAlign: 'right' }, "$max-md": { display: 'none' }, children: size(x.bytes) })] }), _jsx(Acts, { x: x, rows: rows })] }));
229
+ const { t } = useI18n();
230
+ return (_jsxs(XStack, { role: "listitem", items: "center", gap: "$3", px: "$2", py: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, minW: 0, children: [_jsxs(XStack, { render: "button", "aria-label": t('Open {name}', { name: x.name }), onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", cursor: "pointer", "$platform-web": { textAlign: 'start' }, children: [_jsx(Thumb, { x: x, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta(x, t) })] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 72, text: "end", "$max-md": { display: 'none' }, children: size(x.bytes) })] }), _jsx(Acts, { x: x, rows: rows })] }));
216
231
  }
217
232
  /** A chip: one choice among a few, lit when chosen. */
218
233
  function Chip({ label, on, onPress }) {
@@ -220,16 +235,17 @@ function Chip({ label, on, onPress }) {
220
235
  }
221
236
  /** One item, read fresh so its signed URLs are live: the thing itself, then what it is. */
222
237
  export function Preview({ id, rows }) {
223
- const t = useTarget();
238
+ const { t } = useI18n();
239
+ const target = useTarget();
224
240
  const host = useHost();
225
- const one = useRead(host.person ? () => get(t, id) : null, null, [t, id, host.person]);
241
+ const one = useRead(host.person ? () => get(target, id) : null, null, [target, id, host.person]);
226
242
  const x = one.value;
227
243
  if (!x) {
228
- return (_jsx(SizableText, { size: "$2", color: "$soft", children: !host.person ? 'Sign in to open it.' : one.error ? one.error.message : 'Reading…' }));
244
+ return (_jsx(SizableText, { size: "$2", color: "$soft", children: !host.person ? t('Sign in to open it.') : one.error ? told(one.error, t) : t('Reading…') }));
229
245
  }
230
- return (_jsxs(YStack, { gap: "$3", minH: 0, children: [_jsx(Shown, { x: x }), _jsxs(XStack, { items: "flex-start", gap: "$2", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: [meta(x), size(x.bytes), x.width && x.height ? `${x.width}×${x.height}` : '', x.project ? `in ${x.project}` : '', x.shared ? 'shared with the organization' : '']
246
+ return (_jsxs(YStack, { gap: "$3", minH: 0, children: [_jsx(Shown, { x: x }), _jsxs(XStack, { items: "flex-start", gap: "$2", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: [meta(x, t), size(x.bytes), x.width && x.height ? `${x.width}×${x.height}` : '', x.project ? t('in {project}', { project: x.project }) : '', x.shared ? t('shared with the organization') : '']
231
247
  .filter(Boolean)
232
- .join(' · ') })] }), rows ? _jsx(Acts, { x: x, rows: rows(x) }) : null] }), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [x.origin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.open(x.origin), children: [_jsx(ExternalLink, { size: 14 }), "Open where it was made"] })) : null, x.download ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => window.open(x.download, '_blank', 'noopener,noreferrer'), children: [_jsx(Download, { size: 14 }), "Download"] })) : null] })] }));
248
+ .join(' · ') })] }), rows ? _jsx(Acts, { x: x, rows: rows(x) }) : null] }), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [x.origin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.open(x.origin), children: [_jsx(ExternalLink, { size: 14 }), t('Open where it was made')] })) : null, x.download ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => window.open(x.download, '_blank', 'noopener,noreferrer'), children: [_jsx(Download, { size: 14 }), t('Download')] })) : null] })] }));
233
249
  }
234
250
  /** The thing itself, drawn the way its kind is read. */
235
251
  function Shown({ x }) {
@@ -250,6 +266,7 @@ function Shown({ x }) {
250
266
  }
251
267
  /** A text item's first 200 KB, as written; in a frame where the store does not answer this page's read. */
252
268
  function Text({ x }) {
269
+ const { t } = useI18n();
253
270
  const read = useRead(async () => {
254
271
  const res = await fetch(x.url, { cache: 'no-store' });
255
272
  if (!res.ok)
@@ -258,21 +275,22 @@ function Text({ x }) {
258
275
  }, '', [x.url]);
259
276
  if (read.error)
260
277
  return _jsx("iframe", { src: x.url, title: x.name, style: { width: '100%', height: '60vh', border: 0, borderRadius: 12, background: 'white' } });
261
- return (_jsx(YStack, { rounded: "$3", bg: "$raised", p: "$3", maxH: "60vh", overflow: "scroll", children: _jsx(SizableText, { size: "$1", color: "$ink", style: { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: read.loading && !read.value ? 'Reading…' : read.value }) }));
278
+ return (_jsx(YStack, { rounded: "$3", bg: "$raised", p: "$3", maxH: "60vh", overflow: "scroll", children: _jsx(SizableText, { size: "$1", color: "$ink", style: { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: read.loading && !read.value ? t('Reading…') : read.value }) }));
262
279
  }
263
280
  /** A run's outputs as small tiles: each opens in the Library. */
264
281
  export function Outputs({ ids }) {
282
+ const { t } = useI18n();
265
283
  const host = useHost();
266
- const t = useTarget();
284
+ const target = useTarget();
267
285
  const key = ids.join(',');
268
- const got = useRead(host.person && ids.length ? () => Promise.all(ids.slice(0, 12).map((id) => get(t, id).catch(() => null))) : null, [], [t, key, host.person]);
286
+ const got = useRead(host.person && ids.length ? () => Promise.all(ids.slice(0, 12).map((id) => get(target, id).catch(() => null))) : null, [], [target, key, host.person]);
269
287
  const shown = got.value.filter((x) => x !== null);
270
288
  if (!ids.length)
271
289
  return null;
272
- return (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", pt: "$1.5", role: "list", "aria-label": "What it made", children: [shown.map((x) => (_jsx(XStack, { role: "listitem", children: _jsxs(XStack, { render: "button", "aria-label": `Open ${x.name} in the Library`, onPress: () => {
290
+ return (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", pt: "$1.5", role: "list", "aria-label": t('What it made'), children: [shown.map((x) => (_jsx(XStack, { role: "listitem", children: _jsxs(XStack, { render: "button", "aria-label": t('Open {name} in the Library', { name: x.name }), onPress: () => {
273
291
  want(x.id);
274
292
  host.go('-/artifacts');
275
- }, items: "center", gap: "$2", pr: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", overflow: "hidden", hoverStyle: { bg: '$hover' }, cursor: "pointer", maxW: 240, children: [_jsx(Thumb, { x: x, size: 36 }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, children: x.name })] }) }, x.id))), got.loading && !shown.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `Reading ${ids.length === 1 ? 'what it made' : `the ${ids.length} things it made`}…` })) : null] }));
293
+ }, items: "center", gap: "$2", paddingInlineEnd: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", overflow: "hidden", hoverStyle: { bg: '$hover' }, cursor: "pointer", maxW: 240, children: [_jsx(Thumb, { x: x, size: 36 }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, children: x.name })] }) }, x.id))), got.loading && !shown.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: ids.length === 1 ? t('Reading what it made…') : t('Reading the {n} things it made…', { n: ids.length }) })) : null] }));
276
294
  }
277
295
  /** A list read page by page: the first on every change of filter, the next on reaching its end. */
278
296
  function usePages(load, key) {
@@ -337,8 +355,9 @@ function useEnd(on) {
337
355
  return (node) => setEl(node instanceof Element ? node : null);
338
356
  }
339
357
  export function Library() {
358
+ const { t } = useI18n();
340
359
  const host = useHost();
341
- const t = useTarget();
360
+ const target = useTarget();
342
361
  const signed = Boolean(host.person);
343
362
  const wide = useMedia().md;
344
363
  // The kind is the address's (-/artifacts/<kind>), so the sidebar, a link and Back all name it.
@@ -358,8 +377,8 @@ export function Library() {
358
377
  const id = setTimeout(() => setQ(typed.trim()), 250);
359
378
  return () => clearTimeout(id);
360
379
  }, [typed]);
361
- const load = useMemo(() => (signed ? (cursor, signal) => list(t, { kind, source, scope, q, limit: PAGE, cursor }, { signal }) : null), [signed, t, kind, source, scope, q]);
362
- const pages = usePages(load, `${kind}|${source}|${scope}|${q}|${t.org}`);
380
+ const load = useMemo(() => (signed ? (cursor, signal) => list(target, { kind, source, scope, q, limit: PAGE, cursor }, { signal }) : null), [signed, target, kind, source, scope, q]);
381
+ const pages = usePages(load, `${kind}|${source}|${scope}|${q}|${target.org}`);
363
382
  const end = useEnd(pages.more);
364
383
  const reload = pages.reload;
365
384
  useEffect(() => {
@@ -378,8 +397,8 @@ export function Library() {
378
397
  arrived.current = true;
379
398
  const id = named();
380
399
  if (id)
381
- get(t, id).then(look, () => look({ id, name: 'Item' }));
382
- }, [signed, t, look]);
400
+ get(target, id).then(look, () => look({ id, name: t('Item') }));
401
+ }, [signed, target, look, t]);
383
402
  const acts = useActs((x) => {
384
403
  pages.patch(x);
385
404
  if (side.tabs.some((tab) => tab.kind === `${TAB}${x.id}`))
@@ -391,11 +410,14 @@ export function Library() {
391
410
  const kinds = side.tabs
392
411
  .filter((tab) => tab.kind.startsWith(TAB))
393
412
  .map((tab) => ({ id: tab.kind, label: tab.title, render: () => _jsx(Preview, { id: tab.kind.slice(TAB.length), rows: acts.rows }, tab.kind) }));
394
- const panel = (sheet) => _jsx(Panel, { side: side, kinds: kinds, sheet: sheet, onHide: () => setShown(false), label: "Item preview" });
413
+ const panel = (sheet) => _jsx(Panel, { side: side, kinds: kinds, sheet: sheet, onHide: () => setShown(false), label: t('Item preview') });
395
414
  const open = shown && side.tabs.length > 0;
396
415
  const groups = days(pages.rows);
397
416
  const narrowed = Boolean(kind || source || q || scope !== 'all');
398
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsx(YStack, { flex: 1, minH: 0, minW: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { render: "h1", size: "$7", fontWeight: "600", color: "$ink", flex: 1, children: KINDS.find((k) => k.id === kind)?.id ? KINDS.find((k) => k.id === kind).label : 'Library' }), _jsxs(XStack, { role: "radiogroup", "aria-label": "View", gap: "$1", children: [_jsx(ViewMark, { label: "Grid", on: view === 'grid', onPress: () => setView('grid'), children: _jsx(LayoutGrid, { size: 16 }) }), _jsx(ViewMark, { label: "List", on: view === 'list', onPress: () => setView('list'), children: _jsx(List, { size: 16 }) })] })] }), _jsxs(XStack, { items: "center", gap: "$2", maxW: 480, children: [_jsx(Search, { size: 16, opacity: 0.6 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(Input, { value: typed, onChangeText: setTyped, placeholder: "Search your library", "aria-label": "Search your library", disabled: !signed }) })] }), _jsxs(YStack, { gap: "$2", children: [host.sections ? null : (_jsx(XStack, { role: "radiogroup", "aria-label": "Kind", gap: "$2", overflowX: "auto", pb: "$1", "$platform-web": { scrollbarWidth: 'none' }, children: KINDS.map((k) => (_jsx(Chip, { label: k.label, on: kind === k.id, onPress: () => setKind(k.id) }, k.id || 'all'))) })), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [_jsx(ChipSelect, { name: "From", label: FROM.find((f) => f.id === source).label, chosen: FROM.find((f) => f.id === source), items: FROM, onChange: (f) => setSource(f.id), placement: "bottom-start", width: 200 }), _jsx(ChipSelect, { name: "Whose", label: WHOSE.find((w) => w.id === scope).label, chosen: WHOSE.find((w) => w.id === scope), items: WHOSE, onChange: (w) => setScope(w.id), placement: "bottom-start", width: 220 })] })] }), acts.note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: acts.note })) : null, !signed ? (_jsx(Said, { says: "Sign in to see your library.", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) })) : pages.error && !pages.rows.length ? (_jsx(Said, { says: pages.error.message, children: _jsx(Button, { size: "sm", variant: "outline", onPress: pages.reload, children: "Try again" }) })) : pages.loading && !pages.rows.length ? (_jsx(Said, { says: "Reading\u2026" })) : !pages.rows.length ? (_jsx(Said, { says: narrowed ? 'Nothing matches.' : 'Nothing here yet. What you make in Chat, Dev and Studio lands here.' })) : (_jsxs(YStack, { gap: "$5", children: [groups.map((g) => (_jsxs(YStack, { gap: "$3", role: "group", "aria-label": g.label, children: [_jsx(SizableText, { size: "$2", color: "$soft", fontWeight: "500", children: g.label }), view === 'grid' ? (_jsx("div", { role: "list", "aria-label": `Made ${g.label.toLowerCase()}`, style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(46%, 168px), 1fr))', gap: 16 }, children: g.rows.map((x) => (_jsx(Tile, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) })) : (_jsx(YStack, { role: "list", "aria-label": `Made ${g.label.toLowerCase()}`, gap: "$0.5", children: g.rows.map((x) => (_jsx(Row, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) }))] }, g.label))), _jsx(YStack, { ref: end, height: 1 }), pages.loading ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Reading more\u2026" })) : pages.error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: pages.error.message })) : null] })), signed && !q && !kind && (source === '' || source === 'dev') && scope !== 'shared' ? (_jsx(YStack, { pt: "$6", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Sites, {}) })) : null] }) }), open && !wide ? panel(true) : null, open && wide ? (_jsx(YStack, { "data-slot": "library-side", ...pane, ml: gap, shrink: 0, minH: 0, width: 440, maxW: "45%", children: panel(false) })) : null, acts.dialogs] }));
417
+ // The choices in the reader's words; Chat, Dev and Studio stay their names.
418
+ const from = FROM.map((f) => ({ ...f, label: t(f.label) }));
419
+ const whose = WHOSE.map((w) => ({ ...w, label: t(w.label) }));
420
+ return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsx(YStack, { flex: 1, minH: 0, minW: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { render: "h1", size: "$7", fontWeight: "600", color: "$ink", flex: 1, children: KINDS.find((k) => k.id === kind)?.id ? t(KINDS.find((k) => k.id === kind).label) : t('Library') }), _jsxs(XStack, { role: "radiogroup", "aria-label": t('View'), gap: "$1", children: [_jsx(ViewMark, { label: t('Grid'), on: view === 'grid', onPress: () => setView('grid'), children: _jsx(LayoutGrid, { size: 16 }) }), _jsx(ViewMark, { label: t('List'), on: view === 'list', onPress: () => setView('list'), children: _jsx(List, { size: 16 }) })] })] }), _jsxs(XStack, { items: "center", gap: "$2", maxW: 480, children: [_jsx(Search, { size: 16, opacity: 0.6 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(Input, { value: typed, onChangeText: setTyped, placeholder: t('Search your library'), "aria-label": t('Search your library'), disabled: !signed }) })] }), _jsxs(YStack, { gap: "$2", children: [host.sections ? null : (_jsx(XStack, { role: "radiogroup", "aria-label": t('Kind'), gap: "$2", overflowX: "auto", pb: "$1", "$platform-web": { scrollbarWidth: 'none' }, children: KINDS.map((k) => (_jsx(Chip, { label: t(k.label), on: kind === k.id, onPress: () => setKind(k.id) }, k.id || 'all'))) })), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [_jsx(ChipSelect, { name: t('From'), label: from.find((f) => f.id === source).label, chosen: from.find((f) => f.id === source), items: from, onChange: (f) => setSource(f.id), placement: "bottom-start", width: 200 }), _jsx(ChipSelect, { name: t('Whose'), label: whose.find((w) => w.id === scope).label, chosen: whose.find((w) => w.id === scope), items: whose, onChange: (w) => setScope(w.id), placement: "bottom-start", width: 220 })] })] }), acts.note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: acts.note })) : null, !signed ? (_jsx(Said, { says: t('Sign in to see your library.'), children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: t('Sign in') }) })) : pages.error && !pages.rows.length ? (_jsx(Said, { says: told(pages.error, t), children: _jsx(Button, { size: "sm", variant: "outline", onPress: pages.reload, children: t('Try again') }) })) : pages.loading && !pages.rows.length ? (_jsx(Said, { says: t('Reading…') })) : !pages.rows.length ? (_jsx(Said, { says: narrowed ? t('Nothing matches.') : t('Nothing here yet. What you make in Chat, Dev and Studio lands here.') })) : (_jsxs(YStack, { gap: "$5", children: [groups.map((g) => (_jsxs(YStack, { gap: "$3", role: "group", "aria-label": t(g.label), children: [_jsx(SizableText, { size: "$2", color: "$soft", fontWeight: "500", children: t(g.label) }), view === 'grid' ? (_jsx(YStack, { role: "list", "aria-label": made(g.label, t), gap: 16, "$platform-web": { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(46%, 168px), 1fr))' }, children: g.rows.map((x) => (_jsx(Tile, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) })) : (_jsx(YStack, { role: "list", "aria-label": made(g.label, t), gap: "$0.5", children: g.rows.map((x) => (_jsx(Row, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) }))] }, g.label))), _jsx(YStack, { ref: end, height: 1 }), pages.loading ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Reading more…') })) : pages.error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: told(pages.error, t) })) : null] })), signed && !q && !kind && (source === '' || source === 'dev') && scope !== 'shared' ? (_jsx(YStack, { pt: "$6", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Sites, {}) })) : null] }) }), open && !wide ? panel(true) : null, open && wide ? (_jsx(YStack, { "data-slot": "library-side", ...pane, ml: gap, shrink: 0, minH: 0, width: 440, maxW: "45%", children: panel(false) })) : null, acts.dialogs] }));
399
421
  }
400
422
  function ViewMark({ label, on, onPress, children }) {
401
423
  return (_jsx(XStack, { render: "button", role: "radio", "aria-checked": on, "aria-label": label, onPress: onPress, width: 32, height: 32, items: "center", justify: "center", rounded: "$3", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", opacity: on ? 1 : 0.6, children: children }));