@hanzo/build 0.2.79 → 0.2.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +22 -4
  2. package/lib/activity.d.ts +3 -7
  3. package/lib/activity.js +18 -5
  4. package/lib/api/agents.d.ts +36 -35
  5. package/lib/api/agents.js +66 -29
  6. package/lib/api/auto.d.ts +3 -23
  7. package/lib/api/auto.js +39 -22
  8. package/lib/api/billing.d.ts +0 -35
  9. package/lib/api/billing.js +6 -5
  10. package/lib/api/call.d.ts +16 -8
  11. package/lib/api/call.js +32 -1
  12. package/lib/api/capabilities.d.ts +0 -11
  13. package/lib/api/capabilities.js +2 -1
  14. package/lib/api/codebases.d.ts +0 -10
  15. package/lib/api/codebases.js +3 -2
  16. package/lib/api/coding.d.ts +3 -32
  17. package/lib/api/coding.js +13 -7
  18. package/lib/api/connectors.d.ts +1 -17
  19. package/lib/api/connectors.js +5 -4
  20. package/lib/api/environment.d.ts +6 -20
  21. package/lib/api/environment.js +22 -19
  22. package/lib/api/github.d.ts +0 -22
  23. package/lib/api/github.js +3 -2
  24. package/lib/api/keys.d.ts +0 -14
  25. package/lib/api/keys.js +4 -3
  26. package/lib/api/library.d.ts +3 -17
  27. package/lib/api/library.js +23 -22
  28. package/lib/api/machines.d.ts +0 -15
  29. package/lib/api/machines.js +6 -5
  30. package/lib/api/mcp.d.ts +0 -9
  31. package/lib/api/mcp.js +3 -2
  32. package/lib/api/members.d.ts +0 -15
  33. package/lib/api/members.js +4 -3
  34. package/lib/api/memory.d.ts +0 -11
  35. package/lib/api/memory.js +2 -1
  36. package/lib/api/places.d.ts +1 -9
  37. package/lib/api/places.js +12 -1
  38. package/lib/api/plugins.d.ts +1 -16
  39. package/lib/api/plugins.js +4 -3
  40. package/lib/api/profile.d.ts +0 -10
  41. package/lib/api/profile.js +5 -4
  42. package/lib/api/projects.d.ts +0 -14
  43. package/lib/api/projects.js +2 -1
  44. package/lib/api/provider.d.ts +2 -14
  45. package/lib/api/provider.js +12 -10
  46. package/lib/api/sandbox.d.ts +0 -17
  47. package/lib/api/sandbox.js +5 -4
  48. package/lib/api/sessions.d.ts +0 -17
  49. package/lib/api/sessions.js +5 -4
  50. package/lib/api/skills.d.ts +1 -15
  51. package/lib/api/skills.js +5 -4
  52. package/lib/api/studio.d.ts +0 -14
  53. package/lib/api/studio.js +6 -5
  54. package/lib/api/turn.d.ts +6 -28
  55. package/lib/api/turn.js +105 -49
  56. package/lib/api/verdict.d.ts +0 -9
  57. package/lib/api/verdict.js +2 -1
  58. package/lib/api/webhooks.d.ts +1 -14
  59. package/lib/api/webhooks.js +5 -4
  60. package/lib/ask.js +7 -4
  61. package/lib/automations.d.ts +5 -4
  62. package/lib/automations.js +95 -74
  63. package/lib/back.js +10 -4
  64. package/lib/builder.js +7 -5
  65. package/lib/carry.js +20 -16
  66. package/lib/customize/agents.d.ts +3 -0
  67. package/lib/customize/agents.js +152 -55
  68. package/lib/customize/connectors.js +62 -48
  69. package/lib/customize/index.js +45 -14
  70. package/lib/customize/plugins.js +34 -22
  71. package/lib/customize/say.d.ts +13 -1
  72. package/lib/customize/say.js +15 -21
  73. package/lib/customize/skills.js +31 -22
  74. package/lib/customize/ui.d.ts +5 -9
  75. package/lib/customize/ui.js +26 -16
  76. package/lib/data.d.ts +1 -1
  77. package/lib/data.js +3 -2
  78. package/lib/desk.js +45 -33
  79. package/lib/door.js +35 -10
  80. package/lib/environment.js +25 -19
  81. package/lib/files.js +43 -34
  82. package/lib/find.js +12 -9
  83. package/lib/foot.js +7 -4
  84. package/lib/forge.js +61 -40
  85. package/lib/gear.d.ts +2 -1
  86. package/lib/gear.js +6 -4
  87. package/lib/git.js +29 -19
  88. package/lib/host.d.ts +3 -1
  89. package/lib/landing.d.ts +5 -5
  90. package/lib/landing.js +52 -44
  91. package/lib/library.js +73 -51
  92. package/lib/meter.d.ts +1 -1
  93. package/lib/meter.js +14 -5
  94. package/lib/mic.js +12 -5
  95. package/lib/mind.d.ts +1 -0
  96. package/lib/mind.js +5 -3
  97. package/lib/nav.d.ts +6 -5
  98. package/lib/nav.js +37 -30
  99. package/lib/panel.d.ts +2 -1
  100. package/lib/panel.js +52 -36
  101. package/lib/plan.d.ts +4 -20
  102. package/lib/plan.js +26 -10
  103. package/lib/plans.js +49 -30
  104. package/lib/plugins/design.js +3 -2
  105. package/lib/plugins/index.d.ts +23 -4
  106. package/lib/plugins/index.js +11 -26
  107. package/lib/plugins/pdf.js +2 -1
  108. package/lib/plugins/project.js +6 -3
  109. package/lib/plugins/research.js +3 -2
  110. package/lib/plugins/sketch.js +6 -3
  111. package/lib/plugins/slides.js +3 -2
  112. package/lib/plugins/template.js +6 -3
  113. package/lib/plus.js +25 -13
  114. package/lib/prefs.d.ts +0 -14
  115. package/lib/prefs.js +2 -1
  116. package/lib/project.js +69 -62
  117. package/lib/prompt.d.ts +12 -11
  118. package/lib/prompt.js +38 -20
  119. package/lib/publish.js +17 -12
  120. package/lib/reply.js +6 -3
  121. package/lib/run.js +72 -54
  122. package/lib/say.d.ts +2 -0
  123. package/lib/say.js +7 -0
  124. package/lib/scheduled.js +54 -43
  125. package/lib/section.d.ts +1 -0
  126. package/lib/section.js +8 -2
  127. package/lib/settings/account.d.ts +1 -0
  128. package/lib/settings/account.js +31 -21
  129. package/lib/settings/billing.js +55 -28
  130. package/lib/settings/capabilities.js +26 -15
  131. package/lib/settings/card.js +19 -12
  132. package/lib/settings/code.js +22 -11
  133. package/lib/settings/credits.d.ts +4 -4
  134. package/lib/settings/credits.js +34 -21
  135. package/lib/settings/environments.js +21 -14
  136. package/lib/settings/general.js +26 -14
  137. package/lib/settings/index.js +6 -3
  138. package/lib/settings/integrations.js +42 -28
  139. package/lib/settings/keys.js +37 -19
  140. package/lib/settings/list.d.ts +0 -5
  141. package/lib/settings/list.js +24 -15
  142. package/lib/settings/machines.d.ts +3 -0
  143. package/lib/settings/machines.js +63 -38
  144. package/lib/settings/members.js +23 -14
  145. package/lib/settings/memory.js +12 -6
  146. package/lib/settings/notifications.js +32 -19
  147. package/lib/settings/plan.d.ts +1 -1
  148. package/lib/settings/plan.js +40 -17
  149. package/lib/settings/privacy.js +26 -13
  150. package/lib/settings/ui.d.ts +3 -2
  151. package/lib/settings/ui.js +9 -3
  152. package/lib/settings/usage.js +7 -3
  153. package/lib/shelf.js +18 -15
  154. package/lib/side.d.ts +1 -26
  155. package/lib/side.js +11 -5
  156. package/lib/standing.d.ts +2 -10
  157. package/lib/standing.js +5 -3
  158. package/lib/suggest.d.ts +8 -28
  159. package/lib/suggest.js +39 -4
  160. package/lib/suggestions.js +14 -5
  161. package/lib/summary.d.ts +1 -1
  162. package/lib/summary.js +4 -1
  163. package/lib/sync.js +22 -12
  164. package/lib/tools.d.ts +4 -3
  165. package/lib/tools.js +10 -7
  166. package/lib/transcript.js +10 -8
  167. package/package.json +4 -2
@@ -1,25 +1,26 @@
1
1
  import { type SessionStatus } from '@hanzo/ui/chat';
2
2
  import { type Draft, type Kind, type Schedule } from './api/auto.ts';
3
3
  export declare const zone: () => string;
4
+ /** The lists' words are English, marked (`phrase`), and drawn through `t`. */
4
5
  export declare const KINDS: readonly {
5
6
  id: Kind;
6
7
  label: string;
7
8
  }[];
8
- /** A run's status as the rail's dot draws it. */
9
+ /** A run's status as the rail's dot draws it, and in words (marked, drawn through `t`). */
9
10
  export declare const DOT: Record<string, SessionStatus>;
10
11
  export declare const WORD: Record<string, string>;
11
12
  export declare const live: (s: string) => boolean;
12
13
  /** The schedule a kind starts from, keeping whatever of `was` still applies. */
13
14
  export declare function shaped(kind: Kind, was: Schedule): Schedule;
14
- /** What is wrong with a schedule before it is sent, or ''. */
15
+ /** What is wrong with a schedule before it is sent, or '': English, marked (`phrase`), so a screen says it through `t`. */
15
16
  export declare function unready(s: Schedule): string;
16
17
  /** A schedule as the platform reads it: an hourly minute in two digits. */
17
18
  export declare function tidy(d: Draft): Draft;
18
19
  /** A local time and date for an RFC 3339 instant. */
19
20
  export declare const local: (iso: string) => string;
20
- /** A run the platform refused for want of a plan: a free org with no credit for the run fee. */
21
+ /** A run the platform refused for want of a plan: a free org with no credit for the run fee. `PLAN` says so, marked, drawn through `t`. */
21
22
  export declare const unplanned: (e: unknown) => boolean;
22
- export declare const PLAN = "Automations run on a paid plan; on Free each run needs $1 of credit, and this organization has none. Upgrade to run it.";
23
+ export declare const PLAN: string;
23
24
  /** When a task runs, chosen: how often, the day and time, and the zone it is read in. */
24
25
  export declare function When({ s, onChange, disabled }: {
25
26
  s: Schedule;
@@ -10,9 +10,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  * own: a task is its person's, in the org the builder is in. A save sends only
11
11
  * what changed; the switch sends `enabled` alone, so turning one on or off never
12
12
  * changes who it runs as.
13
+ *
14
+ * Its words are the reader's language (@hanzo/i18n `t`); a task's name, its
15
+ * instructions, a run's summary and post and the platform's refusals are drawn
16
+ * as given.
13
17
  */
14
18
  import { SizableText, XStack, YStack } from '@hanzo/gui';
15
19
  import { ChevronLeft } from '@hanzogui/lucide-icons-2';
20
+ import { phrase } from '@hanzo/i18n';
21
+ import { useI18n } from '@hanzo/i18n/react';
16
22
  import { Button, Input, Switch, Textarea } from '@hanzo/ui';
17
23
  import { StatusDot } from '@hanzo/ui/chat';
18
24
  import { ModelPicker } from '@hanzo/ui/models';
@@ -24,7 +30,7 @@ import { current, subscriptions } from './api/billing.js';
24
30
  import { SOCIAL } from './api/provider.js';
25
31
  import { Out } from './out.js';
26
32
  import { useSocial } from './settings/integrations.js';
27
- import { Refusal } from './api/call.js';
33
+ import { Refusal, told } from './api/call.js';
28
34
  import { ENSO, limits as readLimits, models } from './api/models.js';
29
35
  import { Choice, Confirm, Field, Line, Sheet } from './customize/ui.js';
30
36
  import { useRead } from './data.js';
@@ -40,38 +46,39 @@ export const zone = () => {
40
46
  return 'UTC';
41
47
  }
42
48
  };
49
+ /** The lists' words are English, marked (`phrase`), and drawn through `t`. */
43
50
  export const KINDS = [
44
- { id: 'manual', label: 'When I run it' },
45
- { id: 'once', label: 'Once' },
46
- { id: 'hourly', label: 'Every hour' },
47
- { id: 'daily', label: 'Every day' },
48
- { id: 'weekdays', label: 'Weekdays' },
49
- { id: 'weekly', label: 'Every week' },
50
- { id: 'cron', label: 'Cron' },
51
+ { id: 'manual', label: phrase('When I run it') },
52
+ { id: 'once', label: phrase('Once') },
53
+ { id: 'hourly', label: phrase('Every hour') },
54
+ { id: 'daily', label: phrase('Every day') },
55
+ { id: 'weekdays', label: phrase('Weekdays') },
56
+ { id: 'weekly', label: phrase('Every week') },
57
+ { id: 'cron', label: phrase('Cron') },
51
58
  ];
52
59
  const DAYS = [
53
- { id: 'mon', label: 'Monday' },
54
- { id: 'tue', label: 'Tuesday' },
55
- { id: 'wed', label: 'Wednesday' },
56
- { id: 'thu', label: 'Thursday' },
57
- { id: 'fri', label: 'Friday' },
58
- { id: 'sat', label: 'Saturday' },
59
- { id: 'sun', label: 'Sunday' },
60
+ { id: 'mon', label: phrase('Monday') },
61
+ { id: 'tue', label: phrase('Tuesday') },
62
+ { id: 'wed', label: phrase('Wednesday') },
63
+ { id: 'thu', label: phrase('Thursday') },
64
+ { id: 'fri', label: phrase('Friday') },
65
+ { id: 'sat', label: phrase('Saturday') },
66
+ { id: 'sun', label: phrase('Sunday') },
60
67
  ];
61
68
  const PERMISSIONS = [
62
- { id: 'ask', label: 'Ask first' },
63
- { id: 'auto', label: 'Act on its own' },
69
+ { id: 'ask', label: phrase('Ask first') },
70
+ { id: 'auto', label: phrase('Act on its own') },
64
71
  ];
65
- /** A run's status as the rail's dot draws it. */
72
+ /** A run's status as the rail's dot draws it, and in words (marked, drawn through `t`). */
66
73
  export const DOT = { succeeded: 'done', failed: 'error', running: 'running', queued: 'running', review: 'paused', skipped: 'stopped', refused: 'stopped' };
67
74
  export const WORD = {
68
- succeeded: 'Succeeded',
69
- failed: 'Failed',
70
- running: 'Running',
71
- queued: 'Queued',
72
- review: 'Waiting for your review',
73
- skipped: 'Skipped',
74
- refused: 'Refused',
75
+ succeeded: phrase('Succeeded'),
76
+ failed: phrase('Failed'),
77
+ running: phrase('Running'),
78
+ queued: phrase('Queued'),
79
+ review: phrase('Waiting for your review'),
80
+ skipped: phrase('Skipped'),
81
+ refused: phrase('Refused'),
75
82
  };
76
83
  const NAME = Object.fromEntries(SOCIAL.map((p) => [p.id, p.name]));
77
84
  export const live = (s) => s === 'running' || s === 'queued';
@@ -101,14 +108,14 @@ export function shaped(kind, was) {
101
108
  return { kind, cron: was.cron || '0 9 * * 1-5', tz };
102
109
  }
103
110
  }
104
- /** What is wrong with a schedule before it is sent, or ''. */
111
+ /** What is wrong with a schedule before it is sent, or '': English, marked (`phrase`), so a screen says it through `t`. */
105
112
  export function unready(s) {
106
113
  if ((s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly') && !/^([01]?\d|2[0-3]):[0-5]\d$/.test(s.at ?? ''))
107
- return 'Time of day is HH:MM on a 24-hour clock.';
114
+ return phrase('Time of day is HH:MM on a 24-hour clock.');
108
115
  if (s.kind === 'once' && s.at && !/^\d{4}-\d{2}-\d{2}T([01]\d|2[0-3]):[0-5]\d$/.test(s.at))
109
- return 'Once is a date and a time: YYYY-MM-DDTHH:MM.';
116
+ return phrase('Once is a date and a time: YYYY-MM-DDTHH:MM.');
110
117
  if (s.kind === 'cron' && (s.cron ?? '').trim().split(/\s+/).length !== 5)
111
- return 'Cron is five fields: minute, hour, day of month, month, day of week.';
118
+ return phrase('Cron is five fields: minute, hour, day of month, month, day of week.');
112
119
  return '';
113
120
  }
114
121
  /** A schedule as the platform reads it: an hourly minute in two digits. */
@@ -145,13 +152,16 @@ function rebase(cur, was, next) {
145
152
  }
146
153
  return out;
147
154
  }
148
- /** A run the platform refused for want of a plan: a free org with no credit for the run fee. */
155
+ /** A run the platform refused for want of a plan: a free org with no credit for the run fee. `PLAN` says so, marked, drawn through `t`. */
149
156
  export const unplanned = (e) => e instanceof Refusal && e.status === 402;
150
- export const PLAN = 'Automations run on a paid plan; on Free each run needs $1 of credit, and this organization has none. Upgrade to run it.';
157
+ export const PLAN = phrase('Automations run on a paid plan; on Free each run needs $1 of credit, and this organization has none. Upgrade to run it.');
151
158
  const BLANK = () => ({ name: '', instructions: '', model: null, schedule: { kind: 'manual' }, permissions: 'ask', notify: false, postTo: [], enabled: true, source: 'dev' });
152
159
  /** When a task runs, chosen: how often, the day and time, and the zone it is read in. */
153
160
  export function When({ s, onChange, disabled }) {
161
+ const { t } = useI18n();
154
162
  const setWhen = (p) => onChange({ ...s, ...p });
163
+ const kinds = useMemo(() => KINDS.map((k) => ({ ...k, label: t(k.label) })), [t]);
164
+ const days = useMemo(() => DAYS.map((x) => ({ ...x, label: t(x.label) })), [t]);
155
165
  const zones = useMemo(() => {
156
166
  try {
157
167
  return ['UTC', ...Intl.supportedValuesOf('timeZone').filter((z) => z !== 'UTC')].map((z) => ({ id: z, label: z }));
@@ -160,22 +170,23 @@ export function When({ s, onChange, disabled }) {
160
170
  return [{ id: 'UTC', label: 'UTC' }];
161
171
  }
162
172
  }, []);
163
- const kind = KINDS.find((k) => k.id === s.kind) ?? KINDS[0];
164
- return (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", opacity: disabled ? 0.6 : 1, pointerEvents: disabled ? 'none' : 'auto', children: [_jsx(ChipSelect, { name: "When it runs", label: kind.label, chosen: kind, items: KINDS, onChange: (k) => onChange(shaped(k.id, s)), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: "Day", label: DAYS.find((x) => x.id === s.day)?.label ?? 'Monday', chosen: DAYS.find((x) => x.id === s.day), items: DAYS, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": "Time of day", inputMode: "text" }) })) : null, s.kind === 'once' ? (_jsx(YStack, { width: 190, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "2026-10-20T09:00", "aria-label": "Date and time", autoCapitalize: "none" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "at minute" }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": "Minute", inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": "Cron", autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: "Time zone", label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: "Search zones\u2026", placement: "bottom-start", width: 260 }) : null] }));
173
+ const kind = kinds.find((k) => k.id === s.kind) ?? kinds[0];
174
+ return (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", opacity: disabled ? 0.6 : 1, pointerEvents: disabled ? 'none' : 'auto', children: [_jsx(ChipSelect, { name: t('When it runs'), label: kind.label, chosen: kind, items: kinds, onChange: (k) => onChange(shaped(k.id, s)), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: t('Day'), label: days.find((x) => x.id === s.day)?.label ?? t('Monday'), chosen: days.find((x) => x.id === s.day), items: days, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": t('Time of day'), inputMode: "text" }) })) : null, s.kind === 'once' ? (_jsx(YStack, { width: 190, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "2026-10-20T09:00", "aria-label": t('Date and time'), autoCapitalize: "none" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: t('at minute') }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": t('Minute'), inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": t('Cron'), autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: t('Time zone'), label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: t('Search zones…'), placement: "bottom-start", width: 260 }) : null] }));
165
175
  }
166
176
  /** One automation: what it does and when, its switch, Run now, and its runs. `new` writes one. */
167
177
  export function Editor({ id }) {
168
178
  const host = useHost();
169
- const t = useTarget();
179
+ const { t } = useI18n();
180
+ const target = useTarget();
170
181
  const signed = Boolean(host.person);
171
182
  const fresh = id === 'new';
172
183
  // `tick` reads the automation again; `beat` reads its runs again.
173
184
  const [tick, setTick] = useState(0);
174
185
  const [beat, setBeat] = useState(0);
175
- const saved = useRead(signed && !fresh ? () => automation(t, id) : null, null, [t, signed, id, tick]);
176
- const history = useRead(signed && !fresh ? () => runs(t, id) : null, [], [t, signed, id, beat]);
177
- const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
178
- const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
186
+ const saved = useRead(signed && !fresh ? () => automation(target, id) : null, null, [target, signed, id, tick]);
187
+ const history = useRead(signed && !fresh ? () => runs(target, id) : null, [], [target, signed, id, beat]);
188
+ const catalog = useRead(signed ? () => models(target) : null, [], [target, signed]);
189
+ const { limits } = useLimits(signed ? (signal) => readLimits(target, signal) : null, target);
179
190
  // `base` is what the form started from; a save sends what changed since, and only that.
180
191
  const [base, setBase] = useState(BLANK);
181
192
  const [d, setD] = useState(BLANK);
@@ -187,12 +198,12 @@ export function Editor({ id }) {
187
198
  const [leaving, setLeaving] = useState('');
188
199
  const [upgrade, setUpgrade] = useState(false);
189
200
  const social = useSocial(signed, SOCIAL.filter((p) => p.posts).map((p) => p.id));
190
- const subs = useRead(signed ? () => subscriptions(t) : null, [], [t, signed]);
201
+ const subs = useRead(signed ? () => subscriptions(target) : null, [], [target, signed]);
191
202
  // Free is what the platform says when it names no live subscription; a read that failed says nothing.
192
203
  const free = !subs.loading && !subs.error && !current(subs.value);
193
204
  const nameRef = useRef(null);
194
205
  const asksRef = useRef(null);
195
- const offered = useRead(signed && fresh ? () => starters(t) : null, [], [t, signed, fresh]);
206
+ const offered = useRead(signed && fresh ? () => starters(target) : null, [], [target, signed, fresh]);
196
207
  const here = useRef(true);
197
208
  useEffect(() => {
198
209
  here.current = true;
@@ -235,7 +246,7 @@ export function Editor({ id }) {
235
246
  setUpgrade(false);
236
247
  setD((x) => ({ ...x, ...p }));
237
248
  };
238
- const wrong = unready(d.schedule);
249
+ const wrong = t(unready(d.schedule));
239
250
  // A draft waits on its instructions: they have the focus when it opens.
240
251
  const drafty = Boolean(a?.draft);
241
252
  useEffect(() => {
@@ -252,7 +263,7 @@ export function Editor({ id }) {
252
263
  catch (e) {
253
264
  // Wanting a plan is said in these words only where a run was asked for.
254
265
  const run = what === 'run' && unplanned(e);
255
- setNote(run ? PLAN : e.message);
266
+ setNote(run ? t(PLAN) : told(e, t));
256
267
  setUpgrade(run);
257
268
  }
258
269
  finally {
@@ -261,7 +272,7 @@ export function Editor({ id }) {
261
272
  };
262
273
  /** What a save still needs, said, with the focus on where to give it; true when nothing. */
263
274
  const complete = () => {
264
- const ask = !d.name.trim() ? ['Name it first.', nameRef] : !d.instructions.trim() ? ['Add instructions first.', asksRef] : null;
275
+ const ask = !d.name.trim() ? [t('Name it first.'), nameRef] : !d.instructions.trim() ? [t('Add instructions first.'), asksRef] : null;
265
276
  if (ask) {
266
277
  setNote(ask[0]);
267
278
  ask[1].current?.focus();
@@ -275,7 +286,7 @@ export function Editor({ id }) {
275
286
  };
276
287
  const keep = () => act('save', async () => {
277
288
  if (fresh) {
278
- const made = await create(t, tidy(d));
289
+ const made = await create(target, tidy(d));
279
290
  // Only while still here: a person who left during the create stays where they went.
280
291
  if (here.current)
281
292
  host.go(path({ kind: 'automation', id: made.id }), { replace: true });
@@ -285,32 +296,32 @@ export function Editor({ id }) {
285
296
  const send = changes(base, tidy(snap));
286
297
  if (switchesAlone)
287
298
  delete send.enabled;
288
- const now = Object.keys(send).length ? await save(t, id, send) : a;
299
+ const now = Object.keys(send).length ? await save(target, id, send) : a;
289
300
  // What was typed while the save was in flight stands.
290
301
  setBase(drafted(now));
291
302
  setD((cur) => rebase(cur, snap, drafted(now)));
292
303
  setTick((n) => n + 1);
293
- setNote('Saved.');
304
+ setNote(t('Saved.'));
294
305
  });
295
306
  const flip = (on) => {
296
307
  if (!switchesAlone)
297
308
  return set({ enabled: on });
298
309
  void act('switch', async () => {
299
- await save(t, id, { enabled: on });
310
+ await save(target, id, { enabled: on });
300
311
  setTick((n) => n + 1);
301
312
  });
302
313
  };
303
314
  const go = () => act('run', async () => {
304
- const run = await start(t, id);
315
+ const run = await start(target, id);
305
316
  setBeat((n) => n + 1);
306
317
  if (run.status === 'skipped')
307
- setNote('Not started: the previous run is still going.');
318
+ setNote(t('Not started: the previous run is still going.'));
308
319
  });
309
320
  const leave = (how) => host.go(path({ kind: 'screen', screen: 'automations' }), how);
310
321
  // A held post is answered once; its buttons go when it is.
311
322
  const [answered, setAnswered] = useState(() => new Set());
312
323
  const answer = (r, post) => act('review', async () => {
313
- await review(t, id, r.id, post);
324
+ await review(target, id, r.id, post);
314
325
  setAnswered((x) => new Set(x).add(r.id));
315
326
  setBeat((n) => n + 1);
316
327
  setTick((n) => n + 1);
@@ -324,51 +335,61 @@ export function Editor({ id }) {
324
335
  asksRef.current?.focus();
325
336
  };
326
337
  if (!signed)
327
- return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Sign in to see this organization's automations." });
338
+ return _jsx(Page, { title: t('Automation'), onBack: () => leave(), says: t("Sign in to see this organization's automations.") });
328
339
  if (!fresh && saved.error && !a)
329
- return _jsx(Page, { title: "Automation", onBack: () => leave(), says: saved.error.message });
340
+ return _jsx(Page, { title: t('Automation'), onBack: () => leave(), says: told(saved.error, t) });
330
341
  if (!ready)
331
- return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Reading the automation\u2026" });
342
+ return _jsx(Page, { title: t('Automation'), onBack: () => leave(), says: t('Reading the automation…') });
332
343
  const s = d.schedule;
333
344
  // An automation that posts speaks for the organization: only an admin changes it.
334
345
  const locked = !host.admin && Boolean(a?.postTo.length);
335
346
  // A new automation or a draft has nothing to switch on until it says what to do.
336
347
  const can = switchesAlone || Boolean(d.instructions.trim());
337
348
  const on = switchesAlone ? Boolean(a?.enabled) : can && d.enabled;
338
- const says = switchesAlone ? (on ? 'On' : 'Off') : !can ? 'Add instructions first' : on ? 'On when saved' : 'Off when saved';
339
- const state = fresh ? 'Not created yet' : dirty ? 'Unsaved changes' : '';
349
+ const says = switchesAlone ? (on ? t('On') : t('Off')) : !can ? t('Add instructions first') : on ? t('On when saved') : t('Off when saved');
350
+ const state = fresh ? t('Not created yet') : dirty ? t('Unsaved changes') : '';
340
351
  const sayId = `${id}-switch`;
341
- return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { shrink: 0, px: "$6", pt: "$4", pb: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: back, "aria-label": "All scheduled tasks", children: [_jsx(ChevronLeft, { size: 14 }), "Scheduled tasks"] }), _jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: state }), _jsx(XStack, { flex: 1 }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: sayId, size: "$2", color: "$soft", children: says }), _jsx(Switch, { checked: on, disabled: busy === 'switch' || !can, onCheckedChange: flip, "aria-label": switchesAlone ? `${a.name} on` : 'On when saved', "aria-describedby": sayId })] }), a && !a.draft ? (_jsx(Button, { size: "sm", variant: "outline", disabled: Boolean(busy) || dirty, onPress: () => void go(), children: busy === 'run' ? 'Starting…' : 'Run now' })) : null, a && dirty ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => {
352
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { shrink: 0, px: "$6", pt: "$4", pb: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: back, "aria-label": t('All scheduled tasks'), children: [_jsx(ChevronLeft, { size: 14, 'data-i18n-mirror': '' }), t('Scheduled tasks')] }), _jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: state }), _jsx(XStack, { flex: 1 }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: sayId, size: "$2", color: "$soft", children: says }), _jsx(Switch, { checked: on, disabled: busy === 'switch' || !can, onCheckedChange: flip, "aria-label": switchesAlone ? t('{name} on', { name: a.name }) : t('On when saved'), "aria-describedby": sayId })] }), a && !a.draft ? (_jsx(Button, { size: "sm", variant: "outline", disabled: Boolean(busy) || dirty, onPress: () => void go(), children: busy === 'run' ? t('Starting…') : t('Run now') })) : null, a && dirty ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => {
342
353
  setBase(drafted(a));
343
354
  setD(drafted(a));
344
355
  setNote('');
345
- }, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || locked || !(fresh || drafty || dirty), onPress: () => complete() && void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), note ? (_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", pt: "$2", children: [_jsx(Line, { children: note }), upgrade ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => away(path({ kind: 'screen', screen: 'plans' })), children: "See plans" })) : null] })) : null] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$5", children: [_jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$5", children: [locked ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: "This automation posts to the organization\u2019s accounts, so only an org admin changes it." })) : null, free ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$2", children: [_jsx(SizableText, { flex: 1, minW: 200, size: "$1", color: "$soft", children: "Paid plans include automation runs. On the Free plan each run needs $1 of credit." }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => away(path({ kind: 'screen', screen: 'plans' })), children: "See plans" })] })) : null, _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", numberOfLines: 1, children: fresh ? 'New automation' : a?.name }), _jsx(SizableText, { size: "$2", color: "$soft", children: fresh
346
- ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
356
+ }, children: t('Discard changes') })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || locked || !(fresh || drafty || dirty), onPress: () => complete() && void keep(), children: busy === 'save' ? t('Saving…') : fresh ? t('Create') : t('Save') })] }), note ? (_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", pt: "$2", children: [_jsx(Line, { children: note }), upgrade ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => away(path({ kind: 'screen', screen: 'plans' })), children: t('See plans') })) : null] })) : null] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$5", children: [_jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$5", children: [locked ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: t('This automation posts to the organization’s accounts, so only an org admin changes it.') })) : null, free ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$2", children: [_jsx(SizableText, { flex: 1, minW: 200, size: "$1", color: "$soft", children: t('Paid plans include automation runs. On the Free plan each run needs $1 of credit.') }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => away(path({ kind: 'screen', screen: 'plans' })), children: t('See plans') })] })) : null, _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", numberOfLines: 1, children: fresh ? t('New automation') : a?.name }), _jsx(SizableText, { size: "$2", color: "$soft", children: fresh
357
+ ? t('Name it, say what it should do, and when. It runs as you, in this organization.')
347
358
  : a?.draft
348
- ? 'A draft: say what it should do and save it, and it runs.'
349
- : [`From ${SOURCE[a.source]}`, words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), fresh && offered.value.length && !d.instructions.trim() ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Start from" }), offered.value.map((x) => (_jsx(Button, { size: "sm", variant: "outline", onPress: () => begin(x), "aria-label": `Start from ${x.name}`, children: x.name }, x.key)))] })) : null, _jsx(Field, { label: "Name", children: _jsx(Input, { ref: nameRef, value: d.name, onChangeText: (v) => set({ name: v }), onSubmitEditing: () => asksRef.current?.focus(), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh, disabled: locked }) }), _jsx(Field, { label: "Instructions (required)", hint: "What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills.", children: _jsx(Textarea, { ref: asksRef, disabled: locked, value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "What should it do each run? For example: summarize what needs my attention today across calendar, email and messages.", "aria-label": "Instructions", rows: 7 }) }), _jsx(Field, { label: "When it runs", hint: s.kind === 'cron' ? 'Five fields: minute, hour, day of month, month, day of week.' : s.kind === 'once' ? 'It runs at that time and then switches itself off.' : undefined, children: _jsx(When, { s: s, onChange: (next) => set({ schedule: next }), disabled: locked }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: d.model ?? ENSO, onChange: (id) => set({ model: id === ENSO ? null : id }), loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Permissions", hint: d.permissions === 'auto' ? 'Works and uses connectors without stopping.' : 'Reads and researches, changes nothing, and ends with the actions it would take for you to approve.', children: _jsx(Choice, { label: "Permissions", value: d.permissions, options: PERMISSIONS, onChange: (p) => set({ permissions: p }) }) }), _jsxs(Field, { label: "Post to", hint: !host.admin
350
- ? 'An org admin chooses where automations post: the accounts are the organization’s.'
359
+ ? t('A draft: say what it should do and save it, and it runs.')
360
+ : [
361
+ t('From {product}', { product: t(SOURCE[a.source]) }),
362
+ words(a.schedule, t),
363
+ a.next && a.enabled ? t('next {when}', { when: local(a.next) }) : a.schedule.kind === 'manual' ? '' : t('off'),
364
+ ]
365
+ .filter(Boolean)
366
+ .join(' · ') })] }), fresh && offered.value.length && !d.instructions.trim() ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: t('Start from') }), offered.value.map((x) => (_jsx(Button, { size: "sm", variant: "outline", onPress: () => begin(x), "aria-label": t('Start from {name}', { name: x.name }), children: x.name }, x.key)))] })) : null, _jsx(Field, { label: t('Name'), children: _jsx(Input, { ref: nameRef, value: d.name, onChangeText: (v) => set({ name: v }), onSubmitEditing: () => asksRef.current?.focus(), placeholder: t('Morning briefing'), "aria-label": t('Automation name'), autoFocus: fresh, disabled: locked }) }), _jsx(Field, { label: t('Instructions (required)'), hint: t("What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills."), children: _jsx(Textarea, { ref: asksRef, disabled: locked, value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: t('What should it do each run? For example: summarize what needs my attention today across calendar, email and messages.'), "aria-label": t('Instructions'), rows: 7 }) }), _jsx(Field, { label: t('When it runs'), hint: s.kind === 'cron' ? t('Five fields: minute, hour, day of month, month, day of week.') : s.kind === 'once' ? t('It runs at that time and then switches itself off.') : undefined, children: _jsx(When, { s: s, onChange: (next) => set({ schedule: next }), disabled: locked }) }), _jsx(Field, { label: t('Model'), children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: t('Model'), models: catalog.value, scope: "chat", limits: limits, value: d.model ?? ENSO, onChange: (id) => set({ model: id === ENSO ? null : id }), loading: catalog.loading, error: catalog.error ? told(catalog.error, t) : null }) }) }), _jsx(Field, { label: t('Permissions'), hint: d.permissions === 'auto' ? t('Works and uses connectors without stopping.') : t('Reads and researches, changes nothing, and ends with the actions it would take for you to approve.'), children: _jsx(Choice, { label: t('Permissions'), value: d.permissions, options: PERMISSIONS.map((p) => ({ ...p, label: t(p.label) })), onChange: (p) => set({ permissions: p }) }) }), _jsxs(Field, { label: t('Post to'), hint: !host.admin
367
+ ? t('An org admin chooses where automations post: the accounts are the organization’s.')
351
368
  : !d.postTo.length
352
- ? 'Turn an account on and each run’s answer is a post for it.'
369
+ ? t('Turn an account on and each run’s answer is a post for it.')
353
370
  : d.permissions === 'auto'
354
- ? 'Each run posts what the agent writes, as written.'
355
- : 'Each run holds what the agent writes for you to post or discard.', children: [_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: SOCIAL.filter((p) => p.posts).map((p, i) => {
371
+ ? t('Each run posts what the agent writes, as written.')
372
+ : t('Each run holds what the agent writes for you to post or discard.'), children: [_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: SOCIAL.filter((p) => p.posts).map((p, i) => {
356
373
  const c = social.value[p.id];
357
374
  const on = d.postTo.includes(p.id);
358
375
  return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: social.error
359
- ? social.error.message
376
+ ? told(social.error, t)
360
377
  : !c
361
- ? 'Reading…'
378
+ ? t('Reading…')
362
379
  : c.connected
363
- ? `Connected${c.account ? ` as ${c.account}` : ''}`
364
- : `${c.available ? 'Not connected: an org admin connects it in Settings → Integrations.' : p.needs} ${on || d.permissions === 'ask'
365
- ? 'Runs hold the post for you to read; posting it fails until it is connected.'
366
- : 'Acting on its own, it posts only to a connected account.'}` })] }), _jsx(Switch, { checked: on, disabled: !host.admin || (!on && !c?.connected && d.permissions === 'auto'), onCheckedChange: (v) => set({ postTo: v ? [...d.postTo, p.id].sort() : d.postTo.filter((x) => x !== p.id) }), "aria-label": `Post to ${p.name}` })] }, p.id));
367
- }) }), _jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => away(path({ kind: 'settings', section: 'integrations' })), children: "Connect accounts" }) })] }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Email me when a run ends" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "One line on how it went, with a link to the run." })] }), _jsx(Switch, { checked: d.notify, onCheckedChange: (v) => set({ notify: v }), "aria-label": "Email me when a run ends" })] }), dirty && wrong && note !== wrong ? _jsx(Line, { children: wrong }) : null, a ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: "Delete" }) })) : null, a ? (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Runs" }), history.error && !history.value.length ? (_jsx(Line, { children: history.error.message })) : !history.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: history.loading ? 'Reading its runs…' : a.draft ? 'It runs once its instructions are saved.' : 'No runs yet. Run now starts one.' })) : (_jsx(YStack, { role: "list", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", "aria-label": "Run history", children: history.value.map((r, i) => (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(YStack, { pt: "$1", children: _jsx(StatusDot, { status: DOT[r.status] ?? 'idle' }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `${WORD[r.status] ?? r.status} · ${local(r.at)}` }), _jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: r.summary || (live(r.status) ? 'Working…' : '') }), r.draft ? (_jsx(YStack, { mt: "$1.5", px: "$3", py: "$2", rounded: "$2", bg: "$raised", role: "group", "aria-label": "The post", children: _jsx(SizableText, { size: "$2", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: r.draft }) })) : null, r.outputs.length ? _jsx(Outputs, { ids: r.outputs }) : null, r.posts.map((x) => (_jsxs(XStack, { items: "center", gap: "$2", pt: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: NAME[x.to] ?? x.to }), x.url ? (_jsx(Out, { href: x.url, label: `Open the ${NAME[x.to] ?? x.to} post`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Posted \u00B7 open" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.error }))] }, x.to))), r.status === 'review' && host.admin && !answered.has(r.id) ? (_jsxs(XStack, { gap: "$2", pt: "$2", children: [_jsx(Button, { size: "sm", disabled: Boolean(busy), onPress: () => void answer(r, true), "aria-label": `Post the post of ${local(r.at)}`, children: "Post it" }), _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => void answer(r, false), "aria-label": `Discard the post of ${local(r.at)}`, children: "Discard" })] })) : r.status === 'review' && !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", pt: "$1", children: "An org admin posts or discards it." })) : null] }), r.session ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go(r.session), "aria-label": `Open the Dev run of ${local(r.at)}`, children: "Open run" })) : null] }, r.id))) }))] })) : null] }), a ? (_jsx(Confirm, { what: a.name, says: "It stops running, and its schedule and its runs go with it. A run going now is stopped.", open: asking, onOpenChange: setAsking, onYes: async () => {
368
- await remove(t, a.id);
380
+ ? c.account
381
+ ? t('Connected as {account}', { account: c.account })
382
+ : t('Connected')
383
+ : `${c.available ? t('Not connected: an org admin connects it in Settings → Integrations.') : t(p.needs)} ${on || d.permissions === 'ask'
384
+ ? t('Runs hold the post for you to read; posting it fails until it is connected.')
385
+ : t('Acting on its own, it posts only to a connected account.')}` })] }), _jsx(Switch, { checked: on, disabled: !host.admin || (!on && !c?.connected && d.permissions === 'auto'), onCheckedChange: (v) => set({ postTo: v ? [...d.postTo, p.id].sort() : d.postTo.filter((x) => x !== p.id) }), "aria-label": t('Post to {name}', { name: p.name }) })] }, p.id));
386
+ }) }), _jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => away(path({ kind: 'settings', section: 'integrations' })), children: t('Connect accounts') }) })] }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: t('Email me when a run ends') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('One line on how it went, with a link to the run.') })] }), _jsx(Switch, { checked: d.notify, onCheckedChange: (v) => set({ notify: v }), "aria-label": t('Email me when a run ends') })] }), dirty && wrong && note !== wrong ? _jsx(Line, { children: wrong }) : null, a ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: t('Delete') }) })) : null, a ? (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: t('Runs') }), history.error && !history.value.length ? (_jsx(Line, { children: told(history.error, t) })) : !history.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: history.loading ? t('Reading its runs…') : a.draft ? t('It runs once its instructions are saved.') : t('No runs yet. Run now starts one.') })) : (_jsx(YStack, { role: "list", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", "aria-label": t('Run history'), children: history.value.map((r, i) => (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(YStack, { pt: "$1", children: _jsx(StatusDot, { status: DOT[r.status] ?? 'idle' }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `${WORD[r.status] ? t(WORD[r.status]) : r.status} · ${local(r.at)}` }), _jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: r.summary || (live(r.status) ? t('Working…') : '') }), r.draft ? (_jsx(YStack, { mt: "$1.5", px: "$3", py: "$2", rounded: "$2", bg: "$raised", role: "group", "aria-label": t('The post'), children: _jsx(SizableText, { size: "$2", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: r.draft }) })) : null, r.outputs.length ? _jsx(Outputs, { ids: r.outputs }) : null, r.posts.map((x) => (_jsxs(XStack, { items: "center", gap: "$2", pt: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: NAME[x.to] ?? x.to }), x.url ? (_jsx(Out, { href: x.url, label: t('Open the {name} post', { name: NAME[x.to] ?? x.to }), children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: t('Posted · open') }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.error }))] }, x.to))), r.status === 'review' && host.admin && !answered.has(r.id) ? (_jsxs(XStack, { gap: "$2", pt: "$2", children: [_jsx(Button, { size: "sm", disabled: Boolean(busy), onPress: () => void answer(r, true), "aria-label": t('Post the post of {when}', { when: local(r.at) }), children: t('Post it') }), _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => void answer(r, false), "aria-label": t('Discard the post of {when}', { when: local(r.at) }), children: t('Discard') })] })) : r.status === 'review' && !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", pt: "$1", children: t('An org admin posts or discards it.') })) : null] }), r.session ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go(r.session), "aria-label": t('Open the Dev run of {when}', { when: local(r.at) }), children: t('Open run') })) : null] }, r.id))) }))] })) : null] }), a ? (_jsx(Confirm, { what: a.name, says: t('It stops running, and its schedule and its runs go with it. A run going now is stopped.'), open: asking, onOpenChange: setAsking, onYes: async () => {
387
+ await remove(target, a.id);
369
388
  leave({ replace: true });
370
- } })) : null, _jsxs(Sheet, { title: "Leave without saving?", open: Boolean(leaving), onOpenChange: (o) => !o && setLeaving(''), width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: fresh ? 'This automation is not created yet.' : 'Your changes to this automation are not saved.' }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(''), children: "Keep editing" }), _jsx(Button, { size: "sm", variant: "destructive", onPress: () => host.go(leaving), children: "Leave" })] })] })] })] }));
389
+ } })) : null, _jsxs(Sheet, { title: t('Leave without saving?'), open: Boolean(leaving), onOpenChange: (o) => !o && setLeaving(''), width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: fresh ? t('This automation is not created yet.') : t('Your changes to this automation are not saved.') }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(''), children: t('Keep editing') }), _jsx(Button, { size: "sm", variant: "destructive", onPress: () => host.go(leaving), children: t('Leave') })] })] })] })] }));
371
390
  }
391
+ /** A page with nothing to edit yet: its title and what it says come already said. */
372
392
  function Page({ title, says, onBack }) {
373
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$4", children: [_jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: onBack, "aria-label": "All scheduled tasks", children: [_jsx(ChevronLeft, { size: 14 }), "Scheduled tasks"] }) }), _jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }) }));
393
+ const { t } = useI18n();
394
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$4", children: [_jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: onBack, "aria-label": t('All scheduled tasks'), children: [_jsx(ChevronLeft, { size: 14, 'data-i18n-mirror': '' }), t('Scheduled tasks')] }) }), _jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }) }));
374
395
  }
package/lib/back.js CHANGED
@@ -12,6 +12,8 @@
12
12
  * `useBack` reads that answer once, takes it off the address so a reload does
13
13
  * not replay it, finishes a GitHub grant, and says what happened.
14
14
  */
15
+ import { phrase } from '@hanzo/i18n';
16
+ import { useI18n } from '@hanzo/i18n/react';
15
17
  import { useEffect } from 'react';
16
18
  import { complete } from './api/github.js';
17
19
  /** The query keys a return carries its answer in. */
@@ -56,12 +58,13 @@ export function here() {
56
58
  }
57
59
  const NAMES = { github: 'GitHub', slack: 'Slack' };
58
60
  /** A provider id as people write it. */
59
- export const named = (id) => NAMES[id] ?? (id ? id[0].toUpperCase() + id.slice(1) : 'The connection');
61
+ export const named = (id) => NAMES[id] ?? (id ? id[0].toUpperCase() + id.slice(1) : phrase('The connection'));
60
62
  /**
61
63
  * Finish what a return carried, once a person is signed in: `say` gets the
62
64
  * sentence, and `after` runs once something was connected.
63
65
  */
64
66
  export function useBack(t, signed, say, after) {
67
+ const { t: words } = useI18n();
65
68
  useEffect(() => {
66
69
  if (!signed)
67
70
  return;
@@ -72,18 +75,21 @@ export function useBack(t, signed, say, after) {
72
75
  const next = clean(window.location.href);
73
76
  window.history.replaceState(window.history.state, '', next);
74
77
  if (l.kind === 'error') {
75
- say(`${named(l.provider)}: ${l.reason || 'not connected'}`);
78
+ // The provider's own reason is said as it came; with none, ours.
79
+ say(l.reason ? `${words(named(l.provider))}: ${l.reason}` : words('{provider}: not connected', { provider: words(named(l.provider)) }));
76
80
  return;
77
81
  }
78
82
  if (l.kind === 'connected') {
79
- say(`${named(l.provider)} is connected${l.account ? ` as ${l.account}` : ''}`);
83
+ say(l.account
84
+ ? words('{provider} is connected as {account}', { provider: words(named(l.provider)), account: l.account })
85
+ : words('{provider} is connected', { provider: words(named(l.provider)) }));
80
86
  after();
81
87
  return;
82
88
  }
83
89
  if (l.provider !== 'github')
84
90
  return;
85
91
  complete(t, l.grant).then((c) => {
86
- say(c.connected ? `Connected as @${c.login}` : 'GitHub did not connect; connect again');
92
+ say(c.connected ? words('Connected as @{login}', { login: c.login }) : words('GitHub did not connect; connect again'));
87
93
  after();
88
94
  }, (e) => say(e.message));
89
95
  // Once per landing: the address is clean after the first run.
package/lib/builder.js CHANGED
@@ -28,6 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
28
28
  * `DevSection`, section.tsx): one left column, never two.
29
29
  */
30
30
  import { SizableText, XStack, YStack } from '@hanzo/gui';
31
+ import { useI18n } from '@hanzo/i18n/react';
31
32
  import { Menu, Search } from '@hanzogui/lucide-icons-2';
32
33
  import { Button } from '@hanzo/ui';
33
34
  import { SessionRail } from '@hanzo/ui/chat';
@@ -54,6 +55,7 @@ import { Find } from './find.js';
54
55
  import { Slack, useWho, Who } from './foot.js';
55
56
  import { Settings } from './settings/index.js';
56
57
  function Shell() {
58
+ const { t: say } = useI18n();
57
59
  const host = useHost();
58
60
  const t = useTarget();
59
61
  const r = route(host.path);
@@ -69,21 +71,21 @@ function Shell() {
69
71
  host.go(p);
70
72
  };
71
73
  const rows = useMemo(() => {
72
- const list = recents.value.map((s) => ({ id: s.id, title: s.title || 'Untitled run', status: DOTS[s.status] ?? 'idle' }));
74
+ const list = recents.value.map((s) => ({ id: s.id, title: s.title || say('Untitled run'), status: DOTS[s.status] ?? 'idle' }));
73
75
  if (order === 'running')
74
76
  list.sort((a, b) => Number(b.status === 'running') - Number(a.status === 'running'));
75
77
  return list;
76
- }, [recents.value, order]);
78
+ }, [recents.value, order, say]);
77
79
  // The top left, as on claude.ai: the builder's name, which leads to New, and under
78
80
  // it the search that finds a run. The organization is the account's, so it is
79
81
  // switched from the account menu at the foot. Beside the rail from md up, beside
80
82
  // the drawer's button below it — one place at a time.
81
83
  const name = _jsx(Name, { onPress: () => go('') });
82
84
  // Every place in view, none behind a More; the rail's own New starts a run.
83
- const links = nav(host, go)
85
+ const links = nav(host, go, say)
84
86
  .flatMap((g) => g.links)
85
87
  .filter((l) => l.id !== NEW);
86
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), notice: host.person && !slack ? (_jsx(Slack, { onOpen: () => go(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) })) : undefined, account: host.person
88
+ return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? say('Show running first') : say('Show newest first'), empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? say('Sign in to see your runs.') : recents.error ? say(recents.error.message) : recents.loading ? say('Reading…') : say('No runs yet.') }), notice: host.person && !slack ? (_jsx(Slack, { onOpen: () => go(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) })) : undefined, account: host.person
87
89
  ? {
88
90
  // Who, and in which organization, as claude.ai says who and on which plan.
89
91
  name: [host.person.name || host.person.email, host.org].filter(Boolean).join(' · '),
@@ -92,7 +94,7 @@ function Shell() {
92
94
  who.toggle();
93
95
  },
94
96
  }
95
- : { name: 'Sign in', onPress: () => host.signIn?.() }, collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org, logo: host.logo }), pt: collapsed ? undefined : 92, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(YStack, { position: "absolute", t: 10, l: 8, r: 8, z: 2, gap: "$2.5", "$max-md": { display: 'none' }, children: [_jsx(XStack, { px: "$2.5", children: name }), _jsxs(XStack, { render: "button", "aria-label": "Search runs", onPress: () => setFinding(true), items: "center", gap: "$2", px: "$2.5", height: 34, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 15, color: "$soft" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Search" })] })] })), host.person ? _jsx(Who, { open: who.open, onOpenChange: who.onOpenChange, narrow: collapsed }) : null] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 44, px: "$2", gap: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }), name, _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setFinding(true), "aria-label": "Search runs", children: _jsx(Search, { size: 18 }) })] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
97
+ : { name: say('Sign in'), onPress: () => host.signIn?.() }, collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org, logo: host.logo }), pt: collapsed ? undefined : 92, open: drawer, onOpenChange: setDrawer, label: say('Runs') }), collapsed ? null : (_jsxs(YStack, { position: "absolute", t: 10, l: 8, r: 8, z: 2, gap: "$2.5", "$max-md": { display: 'none' }, children: [_jsx(XStack, { px: "$2.5", children: name }), _jsxs(XStack, { render: "button", "aria-label": say('Search runs'), onPress: () => setFinding(true), items: "center", gap: "$2", px: "$2.5", height: 34, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 15, color: "$soft" }), _jsx(SizableText, { size: "$2", color: "$soft", children: say('Search') })] })] })), host.person ? _jsx(Who, { open: who.open, onOpenChange: who.onOpenChange, narrow: collapsed }) : null] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 44, px: "$2", gap: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": say('Open runs'), children: _jsx(Menu, { size: 18 }) }), name, _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setFinding(true), "aria-label": say('Search runs'), children: _jsx(Search, { size: 18 }) })] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
96
98
  }
97
99
  /** The wordmark: the product's name, as its host calls it, which leads to New. */
98
100
  function Name({ onPress }) {