@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.
- package/README.md +13 -0
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +1 -20
- package/lib/api/agents.js +16 -8
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +2 -1
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.js +78 -31
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +43 -10
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +6 -3
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +18 -15
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/automations.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
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 =
|
|
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(
|
|
176
|
-
const history = useRead(signed && !fresh ? () => runs(
|
|
177
|
-
const catalog = useRead(signed ? () => models(
|
|
178
|
-
const { limits } = useLimits(signed ? (signal) => readLimits(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
340
|
+
return _jsx(Page, { title: t('Automation'), onBack: () => leave(), says: told(saved.error, t) });
|
|
330
341
|
if (!ready)
|
|
331
|
-
return _jsx(Page, { title:
|
|
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":
|
|
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:
|
|
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
|
-
: [
|
|
350
|
-
|
|
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
|
|
376
|
+
? told(social.error, t)
|
|
360
377
|
: !c
|
|
361
|
-
? 'Reading…'
|
|
378
|
+
? t('Reading…')
|
|
362
379
|
: c.connected
|
|
363
|
-
?
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 ?
|
|
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:
|
|
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 }) {
|
package/lib/carry.js
CHANGED
|
@@ -17,6 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
* (Chat its agents, Dev its devices) beside these.
|
|
18
18
|
*/
|
|
19
19
|
import { SizableText, XStack } from '@hanzo/gui';
|
|
20
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
20
21
|
import { BookOpen, ListChecks, Palette, ScrollText } from '@hanzogui/lucide-icons-2';
|
|
21
22
|
import { Button, Dialog, DialogContent, DialogDescription, DialogTitle } from '@hanzo/ui';
|
|
22
23
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -65,7 +66,8 @@ async function said(t, skills, memory, mode, design) {
|
|
|
65
66
|
}
|
|
66
67
|
/** Manual's pause: what is about to run, what it does, and the three answers. Closing it is a no. */
|
|
67
68
|
function Allow({ what, onAnswer }) {
|
|
68
|
-
|
|
69
|
+
const { t } = useI18n();
|
|
70
|
+
return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onAnswer('deny'), children: _jsxs(DialogContent, { maxW: 440, width: "100%", "data-slot": "allow", children: [_jsx(DialogTitle, { children: t('Allow {name}?', { name: t(what.name) }) }), _jsx(DialogDescription, { children: what.note ? `${t(what.note)}. ${t('Manual asks before each one runs.')}` : t('Manual asks before each one runs.') }), _jsxs(XStack, { justify: "flex-end", gap: "$2", pt: "$3", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onAnswer('deny'), children: t('Deny') }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => onAnswer('always'), children: _jsx(SizableText, { fontSize: 14, children: t('Always in this chat') }) }), _jsx(Button, { size: "sm", onPress: () => onAnswer('once'), children: t('Allow once') })] })] }) }));
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
73
|
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
@@ -73,6 +75,7 @@ function Allow({ what, onAnswer }) {
|
|
|
73
75
|
* surface offers (a choice it does not offer reads as Auto).
|
|
74
76
|
*/
|
|
75
77
|
export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
|
|
78
|
+
const { t: say } = useI18n();
|
|
76
79
|
const gear = useGear(t, key);
|
|
77
80
|
const [dialog, show] = useState(null);
|
|
78
81
|
// The composer a plugin acts on, read when it acts: the draft moves under it.
|
|
@@ -98,13 +101,13 @@ export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
|
|
|
98
101
|
rows: names.map((n) => ({ id: n, label: n, icon: _jsx(ScrollText, { size: 16 }), on: skills.includes(n) })),
|
|
99
102
|
pick: (id) => gear.toggle('skills', id),
|
|
100
103
|
loading: list.loading,
|
|
101
|
-
error: list.error ? 'The skills could not be read.' : null,
|
|
102
|
-
search: names.length > 8 ? 'Search skills' : undefined,
|
|
103
|
-
empty: 'No skills are on for this organization',
|
|
104
|
-
manage: { label: 'Manage skills', onPress: () => customize('skills') },
|
|
105
|
-
browse: { label: 'Browse skills', onPress: () => customize('skills') },
|
|
104
|
+
error: list.error ? say('The skills could not be read.') : null,
|
|
105
|
+
search: names.length > 8 ? say('Search skills') : undefined,
|
|
106
|
+
empty: say('No skills are on for this organization'),
|
|
107
|
+
manage: { label: say('Manage skills'), onPress: () => customize('skills') },
|
|
108
|
+
browse: { label: say('Browse skills'), onPress: () => customize('skills') },
|
|
106
109
|
};
|
|
107
|
-
const plugins = pluginShelf(found, gear, composer);
|
|
110
|
+
const plugins = pluginShelf(found, gear, composer, say);
|
|
108
111
|
// The design systems: Studio's brands, the one store for them.
|
|
109
112
|
const kits = useRead(signed ? () => brands(t) : null, [], [t, signed, design]);
|
|
110
113
|
const named = kits.value.find((b) => b.id === design);
|
|
@@ -114,26 +117,27 @@ export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
|
|
|
114
117
|
}),
|
|
115
118
|
pick: (id) => gear.set({ design: design === id ? '' : id }),
|
|
116
119
|
loading: kits.loading,
|
|
117
|
-
error: kits.error ? 'The design systems could not be read.' : null,
|
|
118
|
-
empty: 'No design systems yet',
|
|
120
|
+
error: kits.error ? say('The design systems could not be read.') : null,
|
|
121
|
+
empty: say('No design systems yet'),
|
|
119
122
|
create: {
|
|
120
|
-
label: 'Create design system',
|
|
123
|
+
label: say('Create design system'),
|
|
121
124
|
onPress: () => {
|
|
122
125
|
if (!gear.gear.plugins.includes('design'))
|
|
123
126
|
gear.toggle('plugins', 'design');
|
|
124
|
-
|
|
127
|
+
// The person's own words to finish, so in their language.
|
|
128
|
+
lentNow.current.setDraft(say('Make me a design system for '));
|
|
125
129
|
},
|
|
126
130
|
},
|
|
127
131
|
};
|
|
128
132
|
const lit = lighted(found.plugins, gear.gear);
|
|
129
133
|
const on = [
|
|
130
134
|
...skills.map((n) => ({ id: `skill:${n}`, label: n, icon: _jsx(ScrollText, { size: 14 }), onRemove: () => gear.toggle('skills', n) })),
|
|
131
|
-
...(research ? [{ id: 'research', label: 'Research', icon: _jsx(BookOpen, { size: 14 }), onRemove: () => gear.set({ research: false }) }] : []),
|
|
132
|
-
...(mode === 'plan' ? [{ id: 'plan', label: 'Plan', icon: _jsx(ListChecks, { size: 14 }), onRemove: () => gear.set({ mode: 'auto' }) }] : []),
|
|
133
|
-
...(design ? [{ id: 'design', label: named?.name ?? 'Design system', icon: _jsx(Palette, { size: 14 }), onRemove: () => gear.set({ design: '' }) }] : []),
|
|
135
|
+
...(research ? [{ id: 'research', label: say('Research'), icon: _jsx(BookOpen, { size: 14 }), onRemove: () => gear.set({ research: false }) }] : []),
|
|
136
|
+
...(mode === 'plan' ? [{ id: 'plan', label: say('Plan'), icon: _jsx(ListChecks, { size: 14 }), onRemove: () => gear.set({ mode: 'auto' }) }] : []),
|
|
137
|
+
...(design ? [{ id: 'design', label: named?.name ?? say('Design system'), icon: _jsx(Palette, { size: 14 }), onRemove: () => gear.set({ design: '' }) }] : []),
|
|
134
138
|
...lit
|
|
135
139
|
.filter((p) => p.id !== 'research')
|
|
136
|
-
.map((p) => ({ id: `plugin:${p.id}`, label: p.name, icon: p.icon, onRemove: () => gear.toggle('plugins', p.id) })),
|
|
140
|
+
.map((p) => ({ id: `plugin:${p.id}`, label: say(p.name), icon: p.icon, onRemove: () => gear.toggle('plugins', p.id) })),
|
|
137
141
|
];
|
|
138
142
|
const signedIn = Boolean(t.token());
|
|
139
143
|
const [text, setText] = useState('');
|
|
@@ -183,7 +187,7 @@ export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
|
|
|
183
187
|
for (const x of lit)
|
|
184
188
|
if (!signal.aborted && !(await allow({ id: x.id, name: x.name, note: x.note })))
|
|
185
189
|
denied.push(x.id);
|
|
186
|
-
const p = await prep(found.plugins, gear.gear, { ...turn, design }, composer, signal, denied);
|
|
190
|
+
const p = await prep(found.plugins, gear.gear, { ...turn, design }, composer, signal, denied, say);
|
|
187
191
|
if (p.design)
|
|
188
192
|
gear.set({ design: p.design, plugins: gear.gear.plugins.filter((x) => x !== 'design') });
|
|
189
193
|
return p;
|