@hanzo/build 0.2.42 → 0.2.44

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/lib/api/auto.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * DELETE /v1/auto/automations/{id} delete it, its schedule and its runs
9
9
  * POST /v1/auto/automations/{id}/run {run: {id, status}} one run, now
10
10
  * GET /v1/auto/automations/{id}/runs {data: [run]} newest first
11
+ * GET /v1/auto/templates {data: [starter]} to start a new one from
11
12
  *
12
13
  * Each run is one Dev run in this organization, started as the person who last
13
14
  * saved what it does. A draft is a flow named and not yet told what to do: it
@@ -84,3 +85,13 @@ export declare function start(t: Target, id: string): Promise<{
84
85
  export declare function runs(t: Target, id: string): Promise<Run[]>;
85
86
  /** A schedule the way a person says it. */
86
87
  export declare function words(s: Schedule): string;
88
+ /** A ready-made automation to start a new one from. Its schedule names no zone: the page fills in the person's. */
89
+ export interface Starter {
90
+ key: string;
91
+ name: string;
92
+ description: string;
93
+ instructions: string;
94
+ schedule: Schedule;
95
+ }
96
+ /** The starters, from GET /v1/auto/templates. */
97
+ export declare function starters(t: Target): Promise<Starter[]>;
package/lib/api/auto.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * DELETE /v1/auto/automations/{id} delete it, its schedule and its runs
9
9
  * POST /v1/auto/automations/{id}/run {run: {id, status}} one run, now
10
10
  * GET /v1/auto/automations/{id}/runs {data: [run]} newest first
11
+ * GET /v1/auto/templates {data: [starter]} to start a new one from
11
12
  *
12
13
  * Each run is one Dev run in this organization, started as the person who last
13
14
  * saved what it does. A draft is a flow named and not yet told what to do: it
@@ -125,3 +126,12 @@ export function words(s) {
125
126
  return `Cron ${s.cron}${zone}`;
126
127
  }
127
128
  }
129
+ /** The starters, from GET /v1/auto/templates. */
130
+ export async function starters(t) {
131
+ return list(await call(t, 'GET', '/v1/auto/templates'))
132
+ .map((raw) => {
133
+ const o = obj(raw);
134
+ return { key: str(o.key), name: str(o.name), description: str(o.description), instructions: str(o.instructions), schedule: schedule(o.schedule) };
135
+ })
136
+ .filter((x) => x.key && x.name && x.instructions);
137
+ }
@@ -20,10 +20,10 @@ import { ChipSelect } from '@hanzo/ui/product';
20
20
  import { useLimits } from '@hanzo/ui/product/useLimits';
21
21
  import { useEffect, useId, useMemo, useRef, useState } from 'react';
22
22
  import { ago } from './ago.js';
23
- import { automation, automations, changes, create, remove, runs, save, start, words } from './api/auto.js';
23
+ import { automation, automations, changes, create, remove, runs, save, start, starters, words } from './api/auto.js';
24
24
  import { Refusal } from './api/call.js';
25
25
  import { ENSO, limits as readLimits, models } from './api/models.js';
26
- import { Choice, Confirm, Field, Line } from './customize/ui.js';
26
+ import { Choice, Confirm, Field, Line, Sheet } from './customize/ui.js';
27
27
  import { useRead } from './data.js';
28
28
  import { useHost, useTarget } from './host.js';
29
29
  import { path } from './route.js';
@@ -118,12 +118,9 @@ function rebase(cur, was, next) {
118
118
  }
119
119
  return out;
120
120
  }
121
- /** A refusal in the platform's words; out of credit names where to add some. */
122
- function said(e) {
123
- if (e instanceof Refusal && e.status === 402)
124
- return `${e.message || 'This organization is out of credit.'} Add credit in Settings → Billing.`;
125
- return e.message;
126
- }
121
+ /** A run refused for want of credit. */
122
+ const broke = (e) => e instanceof Refusal && e.status === 402;
123
+ const BROKE = 'This organization has no credit for a run. Add credit, then Run now again.';
127
124
  export function Automations() {
128
125
  const host = useHost();
129
126
  const t = useTarget();
@@ -141,7 +138,7 @@ export function Automations() {
141
138
  list.reload();
142
139
  }
143
140
  catch (e) {
144
- setProblem(`${a.name}: ${said(e)}`);
141
+ setProblem(`${a.name}: ${e.message}`);
145
142
  }
146
143
  };
147
144
  return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: "Automations" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Work that runs itself in this organization, on a schedule or when you run it. Each run is a Dev run." })] }), _jsxs(Button, { size: "sm", shrink: 0, disabled: !signed, onPress: () => open('new'), children: [_jsx(Plus, { size: 14 }), "New automation"] })] }), _jsx(XStack, { justify: "flex-end", children: _jsx(YStack, { width: 240, maxW: "100%", children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search\u2026", "aria-label": "Search automations", disabled: !signed }) }) }), _jsx(Line, { children: problem }), list.error ? (_jsx(Empty, { says: list.error.message })) : !signed ? (_jsx(Empty, { says: "Sign in to see this organization's automations." })) : list.loading && list.value.length === 0 ? (_jsx(Empty, { says: "Reading automations\u2026" })) : shown.length === 0 ? (_jsx(Empty, { says: list.value.length === 0 ? 'No automations yet.' : 'Nothing matches.', children: list.value.length === 0 ? (_jsx(Button, { size: "sm", onPress: () => open('new'), children: "New automation" })) : null })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Name" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 220, children: "When" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 150, children: "Last run" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 56, style: { textAlign: 'right' }, children: "On" })] }), shown.map((a) => (_jsx(Item, { a: a, onOpen: () => open(a.id), onSwitch: (on) => void flip(a, on) }, a.id)))] }))] }) }));
@@ -175,6 +172,11 @@ export function Editor({ id }) {
175
172
  const [busy, setBusy] = useState('');
176
173
  const [note, setNote] = useState('');
177
174
  const [asking, setAsking] = useState(false);
175
+ const [leaving, setLeaving] = useState(false);
176
+ const [credit, setCredit] = useState(false);
177
+ const nameRef = useRef(null);
178
+ const asksRef = useRef(null);
179
+ const offered = useRead(signed && fresh ? () => starters(t) : null, [], [t, signed, fresh]);
178
180
  const here = useRef(true);
179
181
  useEffect(() => {
180
182
  here.current = true;
@@ -188,7 +190,7 @@ export function Editor({ id }) {
188
190
  const change = changes(base, d);
189
191
  if (switchesAlone)
190
192
  delete change.enabled;
191
- const dirty = fresh ? Boolean(d.name.trim() || d.instructions.trim()) : Object.keys(change).length > 0;
193
+ const dirty = Object.keys(change).length > 0;
192
194
  // What is saved now becomes the form's start, unless the person is mid-edit: then
193
195
  // their edit stands, and what they did not touch is not sent back over it.
194
196
  const clean = useRef(true);
@@ -211,8 +213,13 @@ export function Editor({ id }) {
211
213
  const timer = setInterval(() => setBeat((n) => n + 1), every);
212
214
  return () => clearInterval(timer);
213
215
  }, [every]);
214
- const set = (p) => setD((x) => ({ ...x, ...p }));
215
- const setWhen = (p) => setD((x) => ({ ...x, schedule: { ...x.schedule, ...p } }));
216
+ // An edit answers what the last line asked for, so the line goes with it.
217
+ const set = (p) => {
218
+ setNote('');
219
+ setCredit(false);
220
+ setD((x) => ({ ...x, ...p }));
221
+ };
222
+ const setWhen = (p) => set({ schedule: { ...d.schedule, ...p } });
216
223
  const wrong = unready(d.schedule);
217
224
  const zones = useMemo(() => {
218
225
  try {
@@ -222,19 +229,43 @@ export function Editor({ id }) {
222
229
  return [{ id: 'UTC', label: 'UTC' }];
223
230
  }
224
231
  }, []);
232
+ // A draft waits on its instructions: they have the focus when it opens.
233
+ const drafty = Boolean(a?.draft);
234
+ useEffect(() => {
235
+ if (ready && drafty)
236
+ asksRef.current?.focus();
237
+ }, [ready, drafty]);
225
238
  const act = async (what, work) => {
226
239
  setBusy(what);
227
240
  setNote('');
241
+ setCredit(false);
228
242
  try {
229
243
  await work();
230
244
  }
231
245
  catch (e) {
232
- setNote(said(e));
246
+ // Wanting credit is said in these words only where a run was asked for.
247
+ const run = what === 'run' && broke(e);
248
+ setNote(run ? BROKE : e.message);
249
+ setCredit(run);
233
250
  }
234
251
  finally {
235
252
  setBusy('');
236
253
  }
237
254
  };
255
+ /** What a save still needs, said, with the focus on where to give it; true when nothing. */
256
+ const complete = () => {
257
+ const ask = !d.name.trim() ? ['Name it first.', nameRef] : !d.instructions.trim() ? ['Add instructions first.', asksRef] : null;
258
+ if (ask) {
259
+ setNote(ask[0]);
260
+ ask[1].current?.focus();
261
+ return false;
262
+ }
263
+ if (wrong) {
264
+ setNote(wrong);
265
+ return false;
266
+ }
267
+ return true;
268
+ };
238
269
  const keep = () => act('save', async () => {
239
270
  if (fresh) {
240
271
  const made = await create(t, tidy(d));
@@ -243,14 +274,14 @@ export function Editor({ id }) {
243
274
  host.go(path({ kind: 'automation', id: made.id }), { replace: true });
244
275
  return;
245
276
  }
246
- const sent = tidy(d);
247
- const send = changes(base, sent);
277
+ const snap = d;
278
+ const send = changes(base, tidy(snap));
248
279
  if (switchesAlone)
249
280
  delete send.enabled;
250
281
  const now = Object.keys(send).length ? await save(t, id, send) : a;
251
282
  // What was typed while the save was in flight stands.
252
283
  setBase(drafted(now));
253
- setD((cur) => rebase(cur, sent, drafted(now)));
284
+ setD((cur) => rebase(cur, snap, drafted(now)));
254
285
  setTick((n) => n + 1);
255
286
  setNote('Saved.');
256
287
  });
@@ -268,26 +299,39 @@ export function Editor({ id }) {
268
299
  if (run.status === 'skipped')
269
300
  setNote('Not started: the previous run is still going.');
270
301
  });
271
- const back = (how) => host.go(path({ kind: 'screen', screen: 'automations' }), how);
302
+ const leave = (how) => host.go(path({ kind: 'screen', screen: 'automations' }), how);
303
+ // Unsaved work is not left without a word.
304
+ const back = () => (dirty ? setLeaving(true) : leave());
305
+ const begin = (x) => {
306
+ const tz = x.schedule.kind === 'manual' ? undefined : zone();
307
+ set({ name: x.name, instructions: x.instructions, schedule: tz ? { ...x.schedule, tz } : { kind: 'manual' } });
308
+ asksRef.current?.focus();
309
+ };
272
310
  if (!signed)
273
- return _jsx(Page, { title: "Automation", onBack: () => back(), says: "Sign in to see this organization's automations." });
311
+ return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Sign in to see this organization's automations." });
274
312
  if (!fresh && saved.error && !a)
275
- return _jsx(Page, { title: "Automation", onBack: () => back(), says: saved.error.message });
313
+ return _jsx(Page, { title: "Automation", onBack: () => leave(), says: saved.error.message });
276
314
  if (!ready)
277
- return _jsx(Page, { title: "Automation", onBack: () => back(), says: "Reading the automation\u2026" });
315
+ return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Reading the automation\u2026" });
278
316
  const s = d.schedule;
279
- const on = switchesAlone ? Boolean(a?.enabled) : d.enabled;
280
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => back(), "aria-label": "All automations", children: [_jsx(ChevronLeft, { size: 14 }), "Automations"] }), _jsx(XStack, { flex: 1 }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: switchesAlone ? (on ? 'On' : 'Off') : on ? 'On when saved' : 'Off when saved' }), _jsx(Switch, { checked: on, disabled: busy === 'switch', onCheckedChange: flip, "aria-label": switchesAlone ? `${a.name} on` : 'On when saved' })] }), a && !a.draft ? (_jsx(Button, { size: "sm", variant: "outline", disabled: Boolean(busy) || dirty, onPress: () => void go(), children: busy === 'run' ? 'Starting…' : 'Run now' })) : 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
281
- ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
282
- : a?.draft
283
- ? 'A draft: say what it should do and save it, and it runs.'
284
- : [words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), _jsx(Field, { label: "Name", children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh }) }), _jsx(Field, { label: "Instructions", 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, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "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.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: 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 === '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] }) }), _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(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" })] }), _jsx(Line, { children: note || (dirty ? wrong : '') }), _jsxs(XStack, { gap: "$2", items: "center", children: [a ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), a && dirty ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => {
317
+ // A new automation or a draft has nothing to switch on until it says what to do.
318
+ const can = switchesAlone || Boolean(d.instructions.trim());
319
+ const on = switchesAlone ? Boolean(a?.enabled) : can && d.enabled;
320
+ const says = switchesAlone ? (on ? 'On' : 'Off') : !can ? 'Add instructions first' : on ? 'On when saved' : 'Off when saved';
321
+ const state = fresh ? 'Not created yet' : dirty ? 'Unsaved changes' : '';
322
+ const sayId = `${id}-switch`;
323
+ 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 automations", children: [_jsx(ChevronLeft, { size: 14 }), "Automations"] }), _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: () => {
285
324
  setBase(drafted(a));
286
325
  setD(drafted(a));
287
- }, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || !dirty || !d.name.trim() || !d.instructions.trim() || Boolean(wrong), onPress: () => void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), 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.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 () => {
288
- await remove(t, a.id);
289
- back({ replace: true });
290
- } })) : null] }));
326
+ setNote('');
327
+ }, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || !(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 }), credit ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Add credit" })) : 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: [_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
328
+ ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
329
+ : a?.draft
330
+ ? 'A draft: say what it should do and save it, and it runs.'
331
+ : [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 }) }), _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, 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.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: 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 === '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] }) }), _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(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.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 () => {
332
+ await remove(t, a.id);
333
+ leave({ replace: true });
334
+ } })) : null, _jsxs(Sheet, { title: "Leave without saving?", open: leaving, onOpenChange: 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(false), children: "Keep editing" }), _jsx(Button, { size: "sm", variant: "destructive", onPress: () => leave(), children: "Leave" })] })] })] })] }));
291
335
  }
292
336
  function Page({ title, says, onBack }) {
293
337
  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 automations", children: [_jsx(ChevronLeft, { size: 14 }), "Automations"] }) }), _jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }) }));
@@ -16,6 +16,6 @@ export function Settings({ section }) {
16
16
  const go = (id) => host.go(path({ kind: 'settings', section: id }));
17
17
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [_jsxs(YStack, { role: "navigation", "aria-label": "Settings", width: 220, shrink: 0, borderRightWidth: 1, borderColor: "$borderColor", px: "$2", py: "$4", gap: "$4", overflow: "scroll", "$max-md": { display: 'none' }, children: [_jsx(SizableText, { size: "$4", color: "$ink", px: "$2", children: "Settings" }), GROUPS.map((g) => {
18
18
  const list = ENTRIES.filter((e) => e.group === g);
19
- return (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g }), list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
19
+ return (_jsxs(YStack, { gap: "$0.5", children: [g === 'Settings' ? null : (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g })), list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
20
20
  })] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
@@ -11,7 +11,7 @@
11
11
  "@hanzo/font": "1.9.3",
12
12
  "@hanzo/gui": "8.3.5",
13
13
  "@hanzo/iam": "0.21.28",
14
- "@hanzo/ui": "8.27.67",
14
+ "@hanzo/ui": "8.27.77",
15
15
  "@hanzo/vite": "0.1.4",
16
16
  "@hanzo/voice": "0.1.8",
17
17
  "@hanzogui/lucide-icons-2": "8.3.5",