@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,16 +5,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  * featured card, a dialog with a title and a scrolling body, a delete that asks
6
6
  * first, the empty state that names the next step, a failure with its retry,
7
7
  * and the quiet line for loading and signed-out.
8
+ *
9
+ * Their own words are the reader's language (@hanzo/i18n `t`); a title, a
10
+ * line or a label a tab hands them comes already said, since it may be a
11
+ * name or a description that is drawn as given.
8
12
  */
9
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
14
+ import { useI18n } from '@hanzo/i18n/react';
10
15
  import { Check, Plus, X } from '@hanzogui/lucide-icons-2';
11
16
  import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
12
17
  import { useEffect, useState } from 'react';
13
18
  import { useHost } from '../host.js';
14
19
  import { say } from './say.js';
15
20
  export const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
16
- /** A button that holds a card's text reads from the left, not from the middle a button centres it in. */
17
- export const left = { textAlign: 'left' };
18
21
  /** Tells the shell how many are yours, whenever that changes. */
19
22
  export function useCount(n, onCount) {
20
23
  useEffect(() => onCount(n), [n, onCount]);
@@ -37,25 +40,29 @@ export function Mark({ name, logo, icon, size = 32 }) {
37
40
  */
38
41
  export function Tile({ title, detail, meta, mark, action, onOpen, }) {
39
42
  const body = (_jsxs(_Fragment, { children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: title }), detail ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 3, children: detail })) : null, meta ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, opacity: 0.8, children: meta })) : null] })] }));
40
- return (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$2", p: "$3", minW: 0, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: onOpen ? { borderColor: '$edge' } : undefined, children: [onOpen ? (_jsx(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 0, gap: "$3", items: "flex-start", cursor: "pointer", style: left, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: body })) : (_jsx(XStack, { flex: 1, minW: 0, gap: "$3", items: "flex-start", children: body })), action] }));
43
+ return (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$2", p: "$3", minW: 0, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: onOpen ? { borderColor: '$edge' } : undefined, children: [onOpen ? (_jsx(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 0, gap: "$3", items: "flex-start", cursor: "pointer", "$platform-web": { textAlign: 'start' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: body })) : (_jsx(XStack, { flex: 1, minW: 0, gap: "$3", items: "flex-start", children: body })), action] }));
41
44
  }
42
45
  /** The one card the shelf puts first, drawn larger. */
43
46
  export function Featured({ title, detail, meta, mark, action, onOpen }) {
44
- return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", style: left, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: "Featured" }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta })] })] }), action] }));
47
+ const { t } = useI18n();
48
+ return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", "$platform-web": { textAlign: 'start' }, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: t('Featured') }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta })] })] }), action] }));
45
49
  }
46
- /** A card's add control: Add while it is not yours, Added once it is. */
47
- export function Add({ name, added, busy, onPress, label = 'Add' }) {
50
+ /** A card's add control: Add (Use, for a template one is made from) while it is not yours, Added once it is. */
51
+ export function Add({ name, added, busy, onPress, use = false }) {
52
+ const { t } = useI18n();
48
53
  if (added) {
49
- return (_jsxs(XStack, { "aria-label": `${name} is added`, gap: "$1", height: 32, items: "center", shrink: 0, children: [_jsx(Check, { size: 14 }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Added" })] }));
54
+ return (_jsxs(XStack, { "aria-label": t('{name} is added', { name }), gap: "$1", height: 32, items: "center", shrink: 0, children: [_jsx(Check, { size: 14 }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Added') })] }));
50
55
  }
51
- return (_jsxs(Button, { size: "sm", variant: "outline", "aria-label": `${label} ${name}`, disabled: busy, onPress: onPress, children: [_jsx(Plus, { size: 14 }), " ", label] }));
56
+ return (_jsxs(Button, { size: "sm", variant: "outline", "aria-label": use ? t('Use {name}', { name }) : t('Add {name}', { name }), disabled: busy, onPress: onPress, children: [_jsx(Plus, { size: 14 }), " ", use ? t('Use') : t('Add')] }));
52
57
  }
53
58
  /** A dialog with a title, a close control and a body that scrolls inside the window. */
54
59
  export function Sheet({ title, open, onOpenChange, width = 560, children }) {
55
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: width, showCloseButton: false, maxH: "88vh", gap: "$3", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { numberOfLines: 1, flex: 1, children: title }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsx(YStack, { gap: "$3", overflow: "scroll", minH: 0, shrink: 1, children: children })] }) }));
60
+ const { t } = useI18n();
61
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: width, showCloseButton: false, maxH: "88vh", gap: "$3", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { numberOfLines: 1, flex: 1, children: title }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsx(YStack, { gap: "$3", overflow: "scroll", minH: 0, shrink: 1, children: children })] }) }));
56
62
  }
57
- /** Asks before a delete; says what goes with it. */
63
+ /** Asks before a delete; says what goes with it. `what` is the thing's name, `says` comes already said. */
58
64
  export function Confirm({ what, says, open, onOpenChange, onYes }) {
65
+ const { t } = useI18n();
59
66
  const [busy, setBusy] = useState(false);
60
67
  const [note, setNote] = useState('');
61
68
  const yes = async () => {
@@ -66,13 +73,13 @@ export function Confirm({ what, says, open, onOpenChange, onYes }) {
66
73
  onOpenChange(false);
67
74
  }
68
75
  catch (e) {
69
- setNote(say(e));
76
+ setNote(say(e, t));
70
77
  }
71
78
  finally {
72
79
  setBusy(false);
73
80
  }
74
81
  };
75
- return (_jsxs(Sheet, { title: `Delete ${what}?`, open: open, onOpenChange: onOpenChange, width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: says }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", variant: "destructive", disabled: busy, onPress: () => void yes(), children: "Delete" })] })] }));
82
+ return (_jsxs(Sheet, { title: t('Delete {name}?', { name: what }), open: open, onOpenChange: onOpenChange, width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: says }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: t('Cancel') }), _jsx(Button, { size: "sm", variant: "destructive", disabled: busy, onPress: () => void yes(), children: t('Delete') })] })] }));
76
83
  }
77
84
  /** The quiet line for loading, a search that matched nothing, and signed-out. */
78
85
  export function Soft({ children, action }) {
@@ -80,7 +87,8 @@ export function Soft({ children, action }) {
80
87
  }
81
88
  /** A read that failed: what went wrong in plain words, and the one control that asks again. */
82
89
  export function Failed({ error, onRetry }) {
83
- return (_jsxs(YStack, { py: "$6", items: "center", gap: "$3", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: say(error) }), _jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: "Try again" })] }));
90
+ const { t } = useI18n();
91
+ return (_jsxs(YStack, { py: "$6", items: "center", gap: "$3", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: say(error, t) }), _jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: t('Try again') })] }));
84
92
  }
85
93
  /** Nothing here yet: what this place holds, and the next step, as buttons. */
86
94
  export function Empty({ title, detail, children }) {
@@ -88,8 +96,9 @@ export function Empty({ title, detail, children }) {
88
96
  }
89
97
  /** What a signed-out visitor is told, with the one control that signs them in. */
90
98
  export function Visitor({ children }) {
99
+ const { t } = useI18n();
91
100
  const host = useHost();
92
- return (_jsx(Soft, { action: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }), children: children }));
101
+ return (_jsx(Soft, { action: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: t('Sign in') }), children: children }));
93
102
  }
94
103
  /** What the last action did, said once, where it was done. */
95
104
  export function Line({ children }) {
@@ -105,9 +114,10 @@ export function Part({ title, detail, action, children }) {
105
114
  export function Field({ label, hint, children }) {
106
115
  return (_jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: label }), children, hint ? (_jsx(SizableText, { size: "$1", color: "$soft", children: hint })) : null] }));
107
116
  }
108
- /** Two or three choices side by side; the chosen one is filled. A count, when one is given, follows its label. */
117
+ /** Two or three choices side by side; the chosen one is filled. A count, when one is given, follows its label. Labels come already said. */
109
118
  export function Choice({ value, options, onChange, label, }) {
110
- return (_jsx(XStack, { role: "group", "aria-label": label, p: 2, gap: 2, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", self: "flex-start", shrink: 0, children: options.map((o) => (_jsxs(XStack, { render: "button", "aria-pressed": o.id === value, "aria-label": o.count ? `${o.label}, ${o.count}` : o.label, onPress: () => onChange(o.id), items: "center", gap: "$1.5", px: "$3", py: "$1", rounded: "$2", bg: o.id === value ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: o.id === value ? '$ink' : '$soft', children: o.label }), o.count ? (_jsx(SizableText, { size: "$1", color: "$soft", fontVariant: ['tabular-nums'], children: o.count })) : null] }, o.id))) }));
119
+ const { t } = useI18n();
120
+ return (_jsx(XStack, { role: "group", "aria-label": label, p: 2, gap: 2, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", self: "flex-start", shrink: 0, children: options.map((o) => (_jsxs(XStack, { render: "button", "aria-pressed": o.id === value, "aria-label": o.count ? t('{label}, {count}', { label: o.label, count: o.count }) : o.label, onPress: () => onChange(o.id), items: "center", gap: "$1.5", px: "$3", py: "$1", rounded: "$2", bg: o.id === value ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: o.id === value ? '$ink' : '$soft', children: o.label }), o.count ? (_jsx(SizableText, { size: "$1", color: "$soft", fontVariant: ['tabular-nums'], children: o.count })) : null] }, o.id))) }));
111
121
  }
112
122
  /** A date from Unix seconds, or ''. */
113
123
  export const day = (s) => (s ? new Date(s * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : '');
package/lib/data.d.ts CHANGED
@@ -12,7 +12,7 @@ export interface Read<T> {
12
12
  export declare function useRead<T>(load: (() => Promise<T>) | null, initial: T, key: unknown[]): Read<T>;
13
13
  /** How long a read is waited on before the screen says it is taking too long. */
14
14
  export declare const WAIT = 15000;
15
- /** A read that did not answer in time. */
15
+ /** A read that did not answer in time. Its message is ours, marked, so a screen drawing it through `t` says it in the reader's language. */
16
16
  export declare class Slow extends Error {
17
17
  constructor();
18
18
  }
package/lib/data.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * signed in is not attempted — the platform would refuse it and the refusal
7
7
  * would read as an outage.
8
8
  */
9
+ import { phrase } from '@hanzo/i18n';
9
10
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
11
  import { places } from './api/places.js';
11
12
  import { projects } from './api/projects.js';
@@ -45,10 +46,10 @@ export function useRead(load, initial, key) {
45
46
  }
46
47
  /** How long a read is waited on before the screen says it is taking too long. */
47
48
  export const WAIT = 15_000;
48
- /** A read that did not answer in time. */
49
+ /** A read that did not answer in time. Its message is ours, marked, so a screen drawing it through `t` says it in the reader's language. */
49
50
  export class Slow extends Error {
50
51
  constructor() {
51
- super('Hanzo is taking too long to answer.');
52
+ super(phrase('Hanzo is taking too long to answer.'));
52
53
  this.name = 'Slow';
53
54
  }
54
55
  }
package/lib/desk.js CHANGED
@@ -8,8 +8,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * The sandbox outlives the run: kept when the run ends, parked while nobody
9
9
  * watches, retired a day after it parks. The bar under the tabs says which and
10
10
  * suspends or resumes it; a tab that needs it running resumes it (sandbox.ts).
11
+ *
12
+ * Its words are the reader's language (@hanzo/i18n `t`); a repository, branch,
13
+ * port, address, command and the sandbox's own error are drawn as given.
11
14
  */
12
15
  import { SizableText, XStack, YStack } from '@hanzo/gui';
16
+ import { useI18n } from '@hanzo/i18n/react';
13
17
  import { ExternalLink, Info, MoreHorizontal, RefreshCcw } from '@hanzogui/lucide-icons-2';
14
18
  import { Button, DropdownMenu } from '@hanzo/ui';
15
19
  import { PreviewFrame } from '@hanzo/ui/agents';
@@ -38,38 +42,39 @@ export const SEED = {
38
42
  const EVERY = 10_000;
39
43
  const PORTS = 5_000;
40
44
  export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, site, published, events, live, menu, refused, retry, onRetry, side, sheet, onHide, }) {
45
+ const { t: say } = useI18n();
41
46
  const name = repo.split('/').filter(Boolean).pop() || repo;
42
47
  const lines = shell(events);
43
- const failed = failure(events);
48
+ const failed = failure(events, say);
44
49
  const box = useBox(sandbox);
45
50
  // Nothing is said of the sandbox until it has been read.
46
51
  const held = box.value?.state ?? '';
47
- const environ = () => side.open({ kind: 'environment', title: 'Environment' });
52
+ const environ = () => side.open({ kind: 'environment', title: say('Environment') });
48
53
  const kinds = [
49
- { id: 'preview', label: 'Preview', bleed: true, render: () => _jsx(Browser, { sandbox: sandbox, held: held, site: site, live: live }, published) },
54
+ { id: 'preview', label: say('Preview'), bleed: true, render: () => _jsx(Browser, { sandbox: sandbox, held: held, site: site, live: live }, published) },
50
55
  {
51
56
  id: 'artifacts',
52
- label: 'Artifacts',
57
+ label: say('Artifacts'),
53
58
  render: () => _jsx(Artifacts, { session: id, sandbox: sandbox, repo: repo, branch: branch, mode: mode, project: project, site: site, pr: pr, onEnvironment: environ }),
54
59
  },
55
- { id: 'files', label: 'Files', render: () => _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: held === 'running' }) },
56
- { id: 'diff', label: 'Diff', render: () => _jsx(Git, { session: id, title: title, live: live }) },
60
+ { id: 'files', label: say('Files'), render: () => _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: held === 'running' }) },
61
+ { id: 'diff', label: say('Diff'), render: () => _jsx(Git, { session: id, title: title, live: live }) },
57
62
  {
58
63
  id: 'terminal',
59
- label: 'Terminal',
64
+ label: say('Terminal'),
60
65
  bleed: true,
61
66
  render: () => _jsx(Terminal, { id: id, sandbox: sandbox, held: held, lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry }),
62
67
  },
63
- { id: 'desktop', label: 'Desktop', bleed: true, render: () => _jsx(Door, { which: "screen", sandbox: sandbox, held: held, session: id }) },
68
+ { id: 'desktop', label: say('Desktop'), bleed: true, render: () => _jsx(Door, { which: "screen", sandbox: sandbox, held: held, session: id }) },
64
69
  {
65
70
  id: 'environment',
66
- label: 'Environment',
67
- render: () => (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Sandbox", value: sandbox || '—' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })),
71
+ label: say('Environment'),
72
+ render: () => (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: say('This run') }), _jsx(Fact, { label: say('Run'), value: id }), _jsx(Fact, { label: say('Repository'), value: name || '—' }), _jsx(Fact, { label: say('Branch'), value: branch || '—' }), _jsx(Fact, { label: say('Base'), value: base || '—' }), _jsx(Fact, { label: say('Runs on'), value: environment || 'sandbox' }), _jsx(Fact, { label: say('Sandbox'), value: sandbox || '—' }), _jsx(Fact, { label: say('Mode'), value: mode || 'build' })] })),
68
73
  },
69
74
  ];
70
- return (_jsx(Panel, { side: side, kinds: kinds, sheet: sheet, onHide: onHide, label: "Run details",
75
+ return (_jsx(Panel, { side: side, kinds: kinds, sheet: sheet, onHide: onHide, label: say('Run details'),
71
76
  // An empty tab offers what this run made: its site and its pull request.
72
- suggested: [...(site ? [{ title: name || site, url: site }] : []), ...(pr.href ? [{ title: `Pull request ${pr.label}`, url: pr.href }] : [])], bar: sandbox ? _jsx(Held, { box: box, live: live }) : null, menu: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: environ }] }) }));
77
+ suggested: [...(site ? [{ title: name || site, url: site }] : []), ...(pr.href ? [{ title: say('Pull request {pr}', { pr: pr.label }), url: pr.href }] : [])], bar: sandbox ? _jsx(Held, { box: box, live: live }) : null, menu: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": say('Run actions'), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: say('Details'), icon: _jsx(Info, { size: 16 }), onSelect: environ }] }) }));
73
78
  }
74
79
  /**
75
80
  * The run's sandbox as it stands, and what moves it. It is read while the pane is
@@ -118,30 +123,33 @@ const until = (ms) => new Date(ms).toLocaleString(undefined, { weekday: 'short',
118
123
  * A sandbox a run is working in is not suspended from here.
119
124
  */
120
125
  function Held({ box, live }) {
126
+ const { t } = useI18n();
121
127
  const b = box.value;
122
128
  const state = b?.state ?? 'pending';
123
129
  const says = state === 'running'
124
- ? 'Sandbox running'
130
+ ? t('Sandbox running')
125
131
  : state === 'parked'
126
132
  ? b?.ends
127
- ? `Sandbox suspended · its files are kept until ${until(b.ends)}`
128
- : 'Sandbox suspended · its files are kept'
133
+ ? t('Sandbox suspended · its files are kept until {time}', { time: until(b.ends) })
134
+ : t('Sandbox suspended · its files are kept')
129
135
  : state === 'pending'
130
- ? 'Sandbox starting…'
136
+ ? t('Sandbox starting…')
131
137
  : state === 'gone'
132
- ? 'Sandbox ended'
133
- : `Sandbox unavailable${b?.error ? ` · ${b.error}` : ''}`;
134
- return (_jsxs(XStack, { px: "$3", py: "$1.5", gap: "$2", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", "aria-label": "Sandbox", children: [_jsx(YStack, { width: 7, height: 7, rounded: 99, bg: state === 'running' ? '$green10' : state === 'parked' ? '$soft' : '$borderColor' }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, children: box.note || says }), state === 'running' && !live ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: box.busy, onPress: () => box.act('park'), children: "Suspend" })) : state === 'parked' ? (_jsx(Button, { size: "sm", variant: "outline", disabled: box.busy, onPress: () => box.act('wake'), children: box.busy ? 'Resuming…' : 'Resume' })) : null] }));
138
+ ? t('Sandbox ended')
139
+ : b?.error
140
+ ? t('Sandbox unavailable · {error}', { error: b.error })
141
+ : t('Sandbox unavailable');
142
+ return (_jsxs(XStack, { px: "$3", py: "$1.5", gap: "$2", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", "aria-label": t('Sandbox'), children: [_jsx(YStack, { width: 7, height: 7, rounded: 99, bg: state === 'running' ? '$green10' : state === 'parked' ? '$soft' : '$borderColor' }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, children: box.note || says }), state === 'running' && !live ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: box.busy, onPress: () => box.act('park'), children: t('Suspend') })) : state === 'parked' ? (_jsx(Button, { size: "sm", variant: "outline", disabled: box.busy, onPress: () => box.act('wake'), children: box.busy ? t('Resuming…') : t('Resume') })) : null] }));
135
143
  }
136
144
  /** What a preview page says of itself, when it cannot serve. */
137
145
  const PREVIEW = 'hanzo-preview';
138
146
  /** How often a preview is opened again on its own before Retry is the person's to press. */
139
147
  const REMINTS = 2;
140
148
  const REMINT = 30_000;
141
- /** What a preview page's status means to the person looking at it, when it is not reopened. */
149
+ /** What a preview page's status means to the person looking at it, when it is not reopened, in the language of `say`. */
142
150
  const SAYS = {
143
- 401: () => 'This browser keeps the preview’s cookie out of a frame. Open it in a new tab.',
144
- 502: (port) => `Nothing answered on port ${port}. Start the server, then Retry.`,
151
+ 401: (_, say) => `${say('This browser keeps the preview’s cookie out of a frame.')} ${say('Open it in a new tab.')}`,
152
+ 502: (port, say) => `${say('Nothing answered on port {port}.', { port })} ${say('Start the server, then Retry.')}`,
145
153
  };
146
154
  /**
147
155
  * The Browser tab: what the run's sandbox serves, one port at a time, framed at
@@ -151,6 +159,7 @@ const SAYS = {
151
159
  * in a while; any other status is said, and Retry is the person's.
152
160
  */
153
161
  function Browser({ sandbox, held, site, live }) {
162
+ const { t: say } = useI18n();
154
163
  const t = useTarget();
155
164
  const [open, setOpen] = useState([]);
156
165
  const [port, setPort] = useState(0);
@@ -211,32 +220,33 @@ function Browser({ sandbox, held, site, live }) {
211
220
  setAttempt((a) => a + 1);
212
221
  return;
213
222
  }
214
- const say = SAYS[m.status];
215
- if (say || m.status === 409)
216
- setWhy(say ? say(port) : 'The sandbox stopped serving. Retry to open it again.');
223
+ const words = SAYS[m.status];
224
+ if (words || m.status === 409)
225
+ setWhy(words ? words(port, say) : `${say('The sandbox stopped serving.')} ${say('Retry to open it again.')}`);
217
226
  };
218
227
  window.addEventListener('message', hear);
219
228
  return () => window.removeEventListener('message', hear);
220
- }, [src, port]);
229
+ }, [src, port, say]);
221
230
  const tab = () => away(preview(t, sandbox, port)).catch((e) => setWhy(e.message));
222
231
  if (!running)
223
232
  return _jsx(Site, { site: site, live: live, held: held });
224
233
  if (!port)
225
234
  return _jsx(Site, { site: site, live: live, held: held });
226
235
  const host = open.find((p) => p.port === port)?.host ?? '';
227
- return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Reload the page", onPress: () => setAttempt((a) => a + 1), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), open.length > 1 ? (_jsx(XStack, { gap: "$1", children: open.map((p) => (_jsx(XStack, { render: "button", "aria-label": `Port ${p.port}`, onPress: () => setPort(p.port), px: "$2", py: "$1", rounded: "$2", bg: p.port === port ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: p.port === port ? '$ink' : '$soft', children: String(p.port) }) }, p.port))) })) : null, _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: host || `port ${port}` }), _jsx(XStack, { render: "button", "aria-label": "Open in a new tab", onPress: () => void tab(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) })] }), why ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: why }), _jsxs(XStack, { gap: "$2", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: "Retry" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => void tab(), children: "Open in a new tab" })] })] })) : src ? (_jsx("iframe", { ref: frame, src: src, title: `What the run serves on port ${port}`, sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#fff' } })) : (_jsx(YStack, { flex: 1, items: "center", justify: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", children: `Opening port ${port}…` }) }))] }));
236
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": say('Reload the page'), onPress: () => setAttempt((a) => a + 1), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), open.length > 1 ? (_jsx(XStack, { gap: "$1", children: open.map((p) => (_jsx(XStack, { render: "button", "aria-label": say('Port {port}', { port: p.port }), onPress: () => setPort(p.port), px: "$2", py: "$1", rounded: "$2", bg: p.port === port ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: p.port === port ? '$ink' : '$soft', children: String(p.port) }) }, p.port))) })) : null, _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: host || say('port {port}', { port }) }), _jsx(XStack, { render: "button", "aria-label": say('Open in a new tab'), onPress: () => void tab(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) })] }), why ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: why }), _jsxs(XStack, { gap: "$2", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: say('Retry') }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => void tab(), children: say('Open in a new tab') })] })] })) : src ? (_jsx("iframe", { ref: frame, src: src, title: say('What the run serves on port {port}', { port }), sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#fff' } })) : (_jsx(YStack, { flex: 1, items: "center", justify: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", children: say('Opening port {port}…', { port }) }) }))] }));
228
237
  }
229
238
  /**
230
239
  * Where the run published its work, framed, with its address, a reload and the
231
240
  * page in a tab of its own; or what keeps its sandbox from serving.
232
241
  */
233
242
  function Site({ site, live, held }) {
243
+ const { t } = useI18n();
234
244
  const frame = useRef(null);
235
- return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Reload the page", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: site || 'Nothing is serving yet' }), site ? (_jsx(Out, { href: site, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: site, title: "Where this run is published", device: "desktop", empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Nothing is serving yet" }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: held === 'parked'
236
- ? 'The run’s sandbox is suspended. Resume it to see what it serves.'
245
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": t('Reload the page'), onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: site || t('Nothing is serving yet') }), site ? (_jsx(Out, { href: site, label: t('Open in a new tab'), children: _jsx(XStack, { p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: site, title: t('Where this run is published'), device: "desktop", empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: t('Nothing is serving yet') }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: held === 'parked'
246
+ ? `${t('The run’s sandbox is suspended.')} ${t('Resume it to see what it serves.')}`
237
247
  : live
238
- ? 'When the run starts a server in its sandbox, the page opens here.'
239
- : 'Nothing listens in this run’s sandbox, and it published no site. Its work is on its branch.' })] }) })] }));
248
+ ? t('When the run starts a server in its sandbox, the page opens here.')
249
+ : `${t('Nothing listens in this run’s sandbox, and it published no site.')} ${t('Its work is on its branch.')}` })] }) })] }));
240
250
  }
241
251
  function Fact({ label, value }) {
242
252
  return (_jsxs(XStack, { gap: "$3", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 96, children: label }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 2, children: value })] }));
@@ -248,14 +258,16 @@ const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace:
248
258
  * shell is a tmux session named for the run, so a reopened tab reattaches to it.
249
259
  */
250
260
  function Terminal({ id, sandbox, held, lines, live, failed, refused, retry, onRetry, }) {
261
+ const { t } = useI18n();
251
262
  const reachable = Boolean(sandbox) && held === 'running';
252
263
  // The shell while there is one, unless the log was asked for.
253
264
  const [log, setLog] = useState(false);
254
265
  const showShell = reachable && !log;
255
- return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [reachable ? (_jsx(XStack, { render: "button", "aria-label": "Shell", onPress: () => setLog(false), px: "$2", py: "$1", rounded: "$2", bg: showShell ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: showShell ? '$ink' : '$soft', children: "Shell" }) })) : null, _jsx(XStack, { render: "button", "aria-label": "Agent log", onPress: () => setLog(true), px: "$2", py: "$1", rounded: "$2", bg: showShell ? 'transparent' : '$hover', children: _jsx(SizableText, { size: "$1", color: showShell ? '$soft' : '$ink', children: "Agent log" }) })] }), showShell ? _jsx(Door, { which: "terminal", sandbox: sandbox, held: held, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })] }));
266
+ return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [reachable ? (_jsx(XStack, { render: "button", "aria-label": t('Shell'), onPress: () => setLog(false), px: "$2", py: "$1", rounded: "$2", bg: showShell ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: showShell ? '$ink' : '$soft', children: t('Shell') }) })) : null, _jsx(XStack, { render: "button", "aria-label": t('Agent log'), onPress: () => setLog(true), px: "$2", py: "$1", rounded: "$2", bg: showShell ? 'transparent' : '$hover', children: _jsx(SizableText, { size: "$1", color: showShell ? '$soft' : '$ink', children: t('Agent log') }) })] }), showShell ? _jsx(Door, { which: "terminal", sandbox: sandbox, held: held, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })] }));
256
267
  }
257
268
  function Log({ lines, live, failed, refused, retry, onRetry, }) {
269
+ const { t } = useI18n();
258
270
  const prompt = 'workspace';
259
271
  const idle = lines.length === 0 && !live;
260
- return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$3", gap: "$1", children: [idle ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", py: "$8", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: refused || (failed ? `This run stopped before its first command: ${failed}` : 'This run finished without writing a command.') }), refused ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: retry })) : null] })) : (lines.map((line, i) => line.role === 'cmd' ? (_jsxs(XStack, { gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(SizableText, { flex: 1, size: "$1", color: "$ink", style: mono, children: line.text })] }, i)) : (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: line.text }, i)))), live ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(YStack, { width: 7, height: 14, bg: "$ink" })] })) : null] }), _jsx(XStack, { px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Commands appear here as the run writes them." }) })] }));
272
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$3", gap: "$1", children: [idle ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", py: "$8", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: refused || (failed ? t('This run stopped before its first command: {reason}', { reason: failed }) : t('This run finished without writing a command.')) }), refused ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: retry })) : null] })) : (lines.map((line, i) => line.role === 'cmd' ? (_jsxs(XStack, { gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(SizableText, { flex: 1, size: "$1", color: "$ink", style: mono, children: line.text })] }, i)) : (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: line.text }, i)))), live ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(YStack, { width: 7, height: 14, bg: "$ink" })] })) : null] }), _jsx(XStack, { px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: t('Commands appear here as the run writes them.') }) })] }));
261
273
  }
package/lib/door.js CHANGED
@@ -8,22 +8,47 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * past the deadline, is a failed connection, and Retry mints a new ticket: a
9
9
  * ticket is spent the moment its socket opens, so reloading the old address can
10
10
  * only fail again.
11
+ *
12
+ * What a door says is the reader's language (@hanzo/i18n `t`), one sentence
13
+ * per door; the page's own reason for a failed connection is drawn as given.
11
14
  */
12
15
  import { SizableText, YStack } from '@hanzo/gui';
16
+ import { phrase } from '@hanzo/i18n';
17
+ import { useI18n } from '@hanzo/i18n/react';
13
18
  import { Button } from '@hanzo/ui';
14
19
  import { useEffect, useRef, useState } from 'react';
15
20
  import { door } from './api/sandbox.js';
16
21
  import { useTarget } from './host.js';
17
22
  /** What each page signs its messages to the window that frames it with. */
18
23
  const SOURCE = { screen: 'hanzo-screen', terminal: 'hanzo-term' };
19
- const WHAT = { screen: 'desktop', terminal: 'terminal' };
20
24
  const DEADLINE = 20_000;
25
+ /** What each door says of itself, one sentence per door, marked (`phrase`) and drawn through `t`. */
26
+ const SAYS = {
27
+ screen: {
28
+ starting: phrase('The desktop opens once the run’s sandbox is up.'),
29
+ parked: phrase('This run’s sandbox is suspended with its files kept. Resume it from the bar above to open its desktop.'),
30
+ error: phrase('This run’s sandbox could not start, so it has no desktop.'),
31
+ unreachable: phrase('Unable to reach the desktop. The run may have stopped.'),
32
+ title: phrase('The run’s desktop'),
33
+ connecting: phrase('Connecting to the desktop…'),
34
+ },
35
+ terminal: {
36
+ starting: phrase('The terminal opens once the run’s sandbox is up.'),
37
+ parked: phrase('This run’s sandbox is suspended with its files kept. Resume it from the bar above to open its terminal.'),
38
+ error: phrase('This run’s sandbox could not start, so it has no terminal.'),
39
+ unreachable: phrase('Unable to reach the terminal. The run may have stopped.'),
40
+ title: phrase('The run’s terminal'),
41
+ connecting: phrase('Connecting to the terminal…'),
42
+ },
43
+ };
21
44
  /**
22
45
  * `held` is where the run's sandbox is in its life, '' until it has been read. Only
23
46
  * a running sandbox has an open door.
24
47
  */
25
48
  export function Door({ which, sandbox, held, session }) {
26
- const t = useTarget();
49
+ const target = useTarget();
50
+ const { t } = useI18n();
51
+ const says = SAYS[which];
27
52
  const frame = useRef(null);
28
53
  const [src, setSrc] = useState('');
29
54
  const [state, setState] = useState('opening');
@@ -37,7 +62,7 @@ export function Door({ which, sandbox, held, session }) {
37
62
  setSrc('');
38
63
  setState('opening');
39
64
  setWhy('');
40
- door(t, sandbox, which, session).then((u) => {
65
+ door(target, sandbox, which, session).then((u) => {
41
66
  if (!gone)
42
67
  setSrc(u);
43
68
  }, (e) => {
@@ -49,7 +74,7 @@ export function Door({ which, sandbox, held, session }) {
49
74
  return () => {
50
75
  gone = true;
51
76
  };
52
- }, [t, sandbox, which, session, up, attempt]);
77
+ }, [target, sandbox, which, session, up, attempt]);
53
78
  useEffect(() => {
54
79
  if (!src)
55
80
  return;
@@ -79,21 +104,21 @@ export function Door({ which, sandbox, held, session }) {
79
104
  };
80
105
  }, [src, which]);
81
106
  if (!sandbox || held === '' || held === 'pending') {
82
- return _jsx(Shut, { title: "Starting", body: `The ${WHAT[which]} opens once the run’s sandbox is up.` });
107
+ return _jsx(Shut, { title: t('Starting'), body: t(says.starting) });
83
108
  }
84
109
  if (held === 'parked') {
85
- return _jsx(Shut, { title: "Suspended", body: `This run’s sandbox is suspended with its files kept. Resume it from the bar above to open its ${WHAT[which]}.` });
110
+ return _jsx(Shut, { title: t('Suspended'), body: t(says.parked) });
86
111
  }
87
112
  if (held === 'gone') {
88
- return _jsx(Shut, { title: "Ended", body: "This run\u2019s sandbox has ended. Its work is on its branch and in its artifacts." });
113
+ return _jsx(Shut, { title: t('Ended'), body: t('This run’s sandbox has ended. Its work is on its branch and in its artifacts.') });
89
114
  }
90
115
  if (held === 'error') {
91
- return _jsx(Shut, { title: "Unavailable", body: `This run’s sandbox could not start, so it has no ${WHAT[which]}.` });
116
+ return _jsx(Shut, { title: t('Unavailable'), body: t(says.error) });
92
117
  }
93
118
  if (state === 'failed') {
94
- return (_jsx(Shut, { title: "Failed to connect", body: why || `Unable to reach the ${WHAT[which]}. The run may have stopped.`, children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: "Retry" }) }));
119
+ return (_jsx(Shut, { title: t('Failed to connect'), body: why || t(says.unreachable), children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: t('Retry') }) }));
95
120
  }
96
- return (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", bg: "#000", children: [src ? (_jsx("iframe", { ref: frame, src: src, title: which === 'screen' ? 'The run’s desktop' : 'The run’s terminal', allow: "clipboard-read; clipboard-write", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#000' } })) : null, state === 'opening' ? (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", pointerEvents: "none", children: _jsx(SizableText, { size: "$2", color: "$soft", children: `Connecting to the ${WHAT[which]}…` }) })) : null] }));
121
+ return (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", bg: "#000", children: [src ? (_jsx("iframe", { ref: frame, src: src, title: t(says.title), allow: "clipboard-read; clipboard-write", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#000' } })) : null, state === 'opening' ? (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", pointerEvents: "none", children: _jsx(SizableText, { size: "$2", color: "$soft", children: t(says.connecting) }) })) : null] }));
97
122
  }
98
123
  function Shut({ title, body, children }) {
99
124
  return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", px: "$6", py: "$8", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 360 }, children: body }), children] }));