@hanzo/build 0.2.57 → 0.2.58

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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The Library: everything a person made, in Chat, Dev, Studio and workflows, in
3
+ * one index.
4
+ *
5
+ * GET /v1/library {data: [item], next} newest first, filtered
6
+ * GET /v1/library/{id} one item, with fresh signed URLs
7
+ * POST /v1/library keep one: its bytes as base64, at most 10 MiB
8
+ * PATCH /v1/library/{id} rename, share with the org, move to a project
9
+ * DELETE /v1/library/{id} its row and its bytes
10
+ *
11
+ * An item is its owner's; another member of the org reads it only once its owner
12
+ * shares it, and changes it never. Its `url`, `download` and `preview` are
13
+ * signed and stop working at `expires`: read the item again for fresh ones.
14
+ */
15
+ import { call, query, seg } from './call.js';
16
+ export const KINDS = ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
17
+ export const SOURCES = ['chat', 'dev', 'studio', 'workflow'];
18
+ /** A product, as a person names it. */
19
+ export const SOURCE = { chat: 'Chat', dev: 'Dev', studio: 'Studio', workflow: 'Workflow' };
20
+ const str = (v) => (typeof v === 'string' ? v : '');
21
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0);
22
+ const obj = (v) => (v && typeof v === 'object' ? v : {});
23
+ /** A signed address: https, and nothing a link could be made to run. */
24
+ const signed = (v) => (/^https:\/\//.test(str(v)) ? str(v) : '');
25
+ /** An app path back to what made an item: under this origin, never another's. */
26
+ export const inApp = (v) => {
27
+ const s = str(v);
28
+ return s.startsWith('/') && !s.startsWith('//') && !s.startsWith('/\\') ? s : '';
29
+ };
30
+ /** One item as the platform answers it, or null for a row with no id. */
31
+ export function item(raw) {
32
+ const o = obj(raw);
33
+ const id = str(o.id);
34
+ if (!id)
35
+ return null;
36
+ const kind = KINDS.includes(str(o.kind)) ? o.kind : 'file';
37
+ const source = SOURCES.includes(str(o.source)) ? o.source : 'dev';
38
+ return {
39
+ id,
40
+ kind,
41
+ source,
42
+ name: str(o.name) || id,
43
+ mime: str(o.mime),
44
+ bytes: num(o.bytes),
45
+ width: num(o.width),
46
+ height: num(o.height),
47
+ seconds: num(o.seconds),
48
+ pages: num(o.pages),
49
+ owner: str(o.owner),
50
+ mine: o.mine === true,
51
+ shared: o.shared === true,
52
+ project: str(o.project),
53
+ ref: str(o.ref),
54
+ origin: inApp(o.origin),
55
+ url: signed(o.url),
56
+ download: signed(o.download),
57
+ preview: signed(o.preview),
58
+ expires: str(o.expires),
59
+ created: str(o.created),
60
+ updated: str(o.updated),
61
+ };
62
+ }
63
+ export async function items(t, f = {}, init) {
64
+ const raw = obj(await call(t, 'GET', `/v1/library${query({
65
+ kind: f.kind,
66
+ source: f.source,
67
+ ref: f.ref,
68
+ project: f.project,
69
+ q: f.q?.trim(),
70
+ since: f.since,
71
+ until: f.until,
72
+ scope: f.scope === 'all' ? undefined : f.scope,
73
+ limit: f.limit,
74
+ cursor: f.cursor,
75
+ })}`, undefined, init));
76
+ const data = (Array.isArray(raw.data) ? raw.data : []).map(item).filter((x) => x !== null);
77
+ return { data, next: str(raw.next) };
78
+ }
79
+ export async function get(t, id) {
80
+ const x = item(await call(t, 'GET', `/v1/library/${seg(id)}`));
81
+ if (!x)
82
+ throw new Error('The item came back without an id');
83
+ return x;
84
+ }
85
+ /** The largest thing one upload carries. */
86
+ export const INLINE = 10 << 20;
87
+ /** Bytes as base64, in chunks a call stack holds. */
88
+ export function base64(bytes) {
89
+ let s = '';
90
+ for (let i = 0; i < bytes.length; i += 0x8000)
91
+ s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
92
+ return btoa(s);
93
+ }
94
+ /** Keep a file in the Library. */
95
+ export async function upload(t, file, made) {
96
+ if (!file.bytes.length)
97
+ throw new Error(`${file.name} is empty`);
98
+ if (file.bytes.length > INLINE)
99
+ throw new Error(`${file.name} is larger than 10 MB`);
100
+ const x = item(await call(t, 'POST', '/v1/library', {
101
+ name: file.name,
102
+ mime: file.type || 'application/octet-stream',
103
+ data: base64(file.bytes),
104
+ ...made,
105
+ }));
106
+ if (!x)
107
+ throw new Error('The item was kept and came back without an id');
108
+ return x;
109
+ }
110
+ export async function change(t, id, c) {
111
+ const x = item(await call(t, 'PATCH', `/v1/library/${seg(id)}`, c));
112
+ if (!x)
113
+ throw new Error('The item was changed and came back without an id');
114
+ return x;
115
+ }
116
+ export async function remove(t, id) {
117
+ await call(t, 'DELETE', `/v1/library/${seg(id)}`);
118
+ }
119
+ /** A size the way a person reads it. */
120
+ export function size(bytes) {
121
+ if (bytes < 1024)
122
+ return `${bytes} B`;
123
+ const units = ['KB', 'MB', 'GB'];
124
+ let n = bytes / 1024;
125
+ let u = 0;
126
+ while (n >= 1024 && u < units.length - 1) {
127
+ n /= 1024;
128
+ u++;
129
+ }
130
+ return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[u]}`;
131
+ }
132
+ /** The day an instant falls on, as a section of the Library: Today, Yesterday, then the month. */
133
+ export function day(iso, now = new Date()) {
134
+ const d = new Date(iso);
135
+ if (Number.isNaN(d.getTime()))
136
+ return 'Earlier';
137
+ const start = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
138
+ const days = Math.round((start(now) - start(d)) / 86_400_000);
139
+ if (days <= 0)
140
+ return 'Today';
141
+ if (days === 1)
142
+ return 'Yesterday';
143
+ return d.toLocaleDateString(undefined, d.getFullYear() === now.getFullYear() ? { month: 'long' } : { month: 'long', year: 'numeric' });
144
+ }
145
+ /** Items under the day each was made, in the order given (newest first). */
146
+ export function days(list, now = new Date()) {
147
+ const out = [];
148
+ for (const x of list) {
149
+ const label = day(x.created, now);
150
+ const last = out[out.length - 1];
151
+ if (last && last.label === label)
152
+ last.rows.push(x);
153
+ else
154
+ out.push({ label, rows: [x] });
155
+ }
156
+ return out;
157
+ }
@@ -1,4 +1,31 @@
1
- export declare function Automations(): import("react").JSX.Element;
1
+ import { type SessionStatus } from '@hanzo/ui/chat';
2
+ import { type Draft, type Kind, type Schedule } from './api/auto.ts';
3
+ export declare const zone: () => string;
4
+ export declare const KINDS: readonly {
5
+ id: Kind;
6
+ label: string;
7
+ }[];
8
+ /** A run's status as the rail's dot draws it. */
9
+ export declare const DOT: Record<string, SessionStatus>;
10
+ export declare const WORD: Record<string, string>;
11
+ export declare const live: (s: string) => boolean;
12
+ /** The schedule a kind starts from, keeping whatever of `was` still applies. */
13
+ export declare function shaped(kind: Kind, was: Schedule): Schedule;
14
+ /** What is wrong with a schedule before it is sent, or ''. */
15
+ export declare function unready(s: Schedule): string;
16
+ /** A schedule as the platform reads it: an hourly minute in two digits. */
17
+ export declare function tidy(d: Draft): Draft;
18
+ /** A local time and date for an RFC 3339 instant. */
19
+ 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
+ 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
+ /** When a task runs, chosen: how often, the day and time, and the zone it is read in. */
24
+ export declare function When({ s, onChange, disabled }: {
25
+ s: Schedule;
26
+ onChange: (s: Schedule) => void;
27
+ disabled?: boolean;
28
+ }): import("react").JSX.Element;
2
29
  /** One automation: what it does and when, its switch, Run now, and its runs. `new` writes one. */
3
30
  export declare function Editor({ id }: {
4
31
  id: string;
@@ -1,26 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Automations: work that runs itself in this organization, on a schedule or
4
- * when someone presses Run. Each run is one Dev run.
3
+ * One scheduled task: what it does and when, its switch, Run now and its runs.
4
+ * The list of them all is Scheduled (scheduled.tsx).
5
5
  *
6
- * -/automations the org's automations; a row opens its editor
7
6
  * -/automations/new a new one: its name, what it does, when it runs
8
7
  * -/automations/<id> its editor, its switch, Run now and its runs
9
8
  *
10
- * Every read and write is /v1/auto/automations (api/auto.ts), scoped to the
11
- * org the builder is in. A save sends only what changed; the switch sends
12
- * `enabled` alone, so turning one on or off never changes who it runs as.
9
+ * Every read and write is /v1/auto/automations (api/auto.ts), the caller's
10
+ * own: a task is its person's, in the org the builder is in. A save sends only
11
+ * what changed; the switch sends `enabled` alone, so turning one on or off never
12
+ * changes who it runs as.
13
13
  */
14
14
  import { SizableText, XStack, YStack } from '@hanzo/gui';
15
- import { ChevronLeft, Plus, Workflow } from '@hanzogui/lucide-icons-2';
15
+ import { ChevronLeft } from '@hanzogui/lucide-icons-2';
16
16
  import { Button, Input, Switch, Textarea } from '@hanzo/ui';
17
17
  import { StatusDot } from '@hanzo/ui/chat';
18
18
  import { ModelPicker } from '@hanzo/ui/models';
19
19
  import { ChipSelect } from '@hanzo/ui/product';
20
20
  import { useLimits } from '@hanzo/ui/product/useLimits';
21
- import { useEffect, useId, useMemo, useRef, useState } from 'react';
22
- import { ago } from './ago.js';
23
- import { automation, automations, changes, create, remove, review, runs, save, start, starters, words } from './api/auto.js';
21
+ import { useEffect, useMemo, useRef, useState } from 'react';
22
+ import { automation, changes, create, remove, review, runs, save, start, starters, words } from './api/auto.js';
24
23
  import { current, subscriptions } from './api/billing.js';
25
24
  import { SOCIAL } from './api/provider.js';
26
25
  import { Out } from './out.js';
@@ -30,8 +29,10 @@ import { ENSO, limits as readLimits, models } from './api/models.js';
30
29
  import { Choice, Confirm, Field, Line, Sheet } from './customize/ui.js';
31
30
  import { useRead } from './data.js';
32
31
  import { useHost, useTarget } from './host.js';
32
+ import { SOURCE } from './api/library.js';
33
+ import { Outputs } from './library.js';
33
34
  import { path } from './route.js';
34
- const zone = () => {
35
+ export const zone = () => {
35
36
  try {
36
37
  return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
37
38
  }
@@ -39,8 +40,9 @@ const zone = () => {
39
40
  return 'UTC';
40
41
  }
41
42
  };
42
- const KINDS = [
43
+ export const KINDS = [
43
44
  { id: 'manual', label: 'When I run it' },
45
+ { id: 'once', label: 'Once' },
44
46
  { id: 'hourly', label: 'Every hour' },
45
47
  { id: 'daily', label: 'Every day' },
46
48
  { id: 'weekdays', label: 'Weekdays' },
@@ -61,8 +63,8 @@ const PERMISSIONS = [
61
63
  { id: 'auto', label: 'Act on its own' },
62
64
  ];
63
65
  /** A run's status as the rail's dot draws it. */
64
- const DOT = { succeeded: 'done', failed: 'error', running: 'running', queued: 'running', review: 'paused', skipped: 'stopped', refused: 'stopped' };
65
- const WORD = {
66
+ export const DOT = { succeeded: 'done', failed: 'error', running: 'running', queued: 'running', review: 'paused', skipped: 'stopped', refused: 'stopped' };
67
+ export const WORD = {
66
68
  succeeded: 'Succeeded',
67
69
  failed: 'Failed',
68
70
  running: 'Running',
@@ -72,14 +74,22 @@ const WORD = {
72
74
  refused: 'Refused',
73
75
  };
74
76
  const NAME = Object.fromEntries(SOCIAL.map((p) => [p.id, p.name]));
75
- const live = (s) => s === 'running' || s === 'queued';
77
+ export const live = (s) => s === 'running' || s === 'queued';
78
+ /** A local date and time an hour from now, as a once schedule's `at`. */
79
+ const soon = () => {
80
+ const d = new Date(Date.now() + 3_600_000);
81
+ const two = (n) => String(n).padStart(2, '0');
82
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}T${two(d.getHours())}:00`;
83
+ };
76
84
  /** The schedule a kind starts from, keeping whatever of `was` still applies. */
77
- function shaped(kind, was) {
85
+ export function shaped(kind, was) {
78
86
  // A schedule that runs only when asked keeps no zone worth keeping: leaving it takes the person's own.
79
87
  const tz = was.kind === 'manual' || !was.tz ? zone() : was.tz;
80
88
  switch (kind) {
81
89
  case 'manual':
82
90
  return { kind };
91
+ case 'once':
92
+ return { kind, at: soon(), tz };
83
93
  case 'hourly':
84
94
  return { kind, at: `00:${(was.at ?? '00:00').slice(3) || '00'}`, tz };
85
95
  case 'daily':
@@ -92,15 +102,17 @@ function shaped(kind, was) {
92
102
  }
93
103
  }
94
104
  /** What is wrong with a schedule before it is sent, or ''. */
95
- function unready(s) {
105
+ export function unready(s) {
96
106
  if ((s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly') && !/^([01]?\d|2[0-3]):[0-5]\d$/.test(s.at ?? ''))
97
107
  return 'Time of day is HH:MM on a 24-hour clock.';
108
+ 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.';
98
110
  if (s.kind === 'cron' && (s.cron ?? '').trim().split(/\s+/).length !== 5)
99
111
  return 'Cron is five fields: minute, hour, day of month, month, day of week.';
100
112
  return '';
101
113
  }
102
114
  /** A schedule as the platform reads it: an hourly minute in two digits. */
103
- function tidy(d) {
115
+ export function tidy(d) {
104
116
  const s = d.schedule;
105
117
  if (s.kind !== 'hourly')
106
118
  return d;
@@ -108,7 +120,7 @@ function tidy(d) {
108
120
  return { ...d, schedule: { ...s, at: `00:${String(m).padStart(2, '0')}` } };
109
121
  }
110
122
  /** A local time and date for an RFC 3339 instant. */
111
- const local = (iso) => {
123
+ export const local = (iso) => {
112
124
  const d = new Date(iso);
113
125
  return Number.isNaN(d.getTime()) ? '' : d.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
114
126
  };
@@ -122,6 +134,7 @@ const drafted = (a) => ({
122
134
  notify: a.notify,
123
135
  postTo: a.postTo,
124
136
  enabled: a.draft ? true : a.enabled,
137
+ source: a.source,
125
138
  });
126
139
  /** `next`, keeping each field of `cur` the person changed from `was` since. */
127
140
  function rebase(cur, was, next) {
@@ -133,39 +146,23 @@ function rebase(cur, was, next) {
133
146
  return out;
134
147
  }
135
148
  /** A run the platform refused for want of a plan: a free org with no credit for the run fee. */
136
- const unplanned = (e) => e instanceof Refusal && e.status === 402;
137
- 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.';
138
- export function Automations() {
139
- const host = useHost();
140
- const t = useTarget();
141
- const signed = Boolean(host.person);
142
- const [q, setQ] = useState('');
143
- const [problem, setProblem] = useState('');
144
- const list = useRead(signed ? () => automations(t) : null, [], [t, signed]);
145
- const needle = q.trim().toLowerCase();
146
- const shown = list.value.filter((a) => !needle || a.name.toLowerCase().includes(needle) || a.instructions.toLowerCase().includes(needle));
147
- const open = (id) => host.go(path({ kind: 'automation', id }));
148
- const flip = async (a, on) => {
149
- setProblem('');
149
+ 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.';
151
+ const BLANK = () => ({ name: '', instructions: '', model: null, schedule: { kind: 'manual' }, permissions: 'ask', notify: false, postTo: [], enabled: true, source: 'dev' });
152
+ /** When a task runs, chosen: how often, the day and time, and the zone it is read in. */
153
+ export function When({ s, onChange, disabled }) {
154
+ const setWhen = (p) => onChange({ ...s, ...p });
155
+ const zones = useMemo(() => {
150
156
  try {
151
- await save(t, a.id, { enabled: on });
152
- list.reload();
157
+ return ['UTC', ...Intl.supportedValuesOf('timeZone').filter((z) => z !== 'UTC')].map((z) => ({ id: z, label: z }));
153
158
  }
154
- catch (e) {
155
- setProblem(`${a.name}: ${e.message}`);
159
+ catch {
160
+ return [{ id: 'UTC', label: 'UTC' }];
156
161
  }
157
- };
158
- 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)))] }))] }) }));
159
- }
160
- /** One automation in the list: its name opens it, and its switch arms it. */
161
- function Item({ a, onOpen, onSwitch }) {
162
- const about = useId();
163
- return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${a.name}`, "aria-describedby": about, onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, minW: 120, style: { textAlign: 'left' }, children: a.name }), _jsxs(XStack, { id: about, items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$1", maxW: "100%", children: [a.draft ? (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: "Draft" })) : null, _jsx(SizableText, { size: "$1", color: "$soft", width: 220, numberOfLines: 1, style: { textAlign: 'left' }, children: a.draft ? 'Needs instructions' : words(a.schedule) }), _jsxs(XStack, { width: 150, items: "center", gap: "$2", children: [a.last ? _jsx(StatusDot, { status: DOT[a.last.status] ?? 'idle' }) : null, _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: a.last ? `${WORD[a.last.status] ?? a.last.status} · ${ago(a.last.at)}` : 'Never run' })] })] })] }), _jsx(XStack, { width: 56, justify: "flex-end", children: _jsx(Switch, { checked: a.enabled, disabled: a.draft, onCheckedChange: onSwitch, "aria-label": `${a.name} on` }) })] }));
164
- }
165
- function Empty({ says, children }) {
166
- return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$4", px: "$4", py: "$8", gap: "$3", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: says }), children] }));
162
+ }, []);
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] }));
167
165
  }
168
- const BLANK = () => ({ name: '', instructions: '', model: null, schedule: { kind: 'manual' }, permissions: 'ask', notify: false, postTo: [], enabled: true });
169
166
  /** One automation: what it does and when, its switch, Run now, and its runs. `new` writes one. */
170
167
  export function Editor({ id }) {
171
168
  const host = useHost();
@@ -238,16 +235,7 @@ export function Editor({ id }) {
238
235
  setUpgrade(false);
239
236
  setD((x) => ({ ...x, ...p }));
240
237
  };
241
- const setWhen = (p) => set({ schedule: { ...d.schedule, ...p } });
242
238
  const wrong = unready(d.schedule);
243
- const zones = useMemo(() => {
244
- try {
245
- return ['UTC', ...Intl.supportedValuesOf('timeZone').filter((z) => z !== 'UTC')].map((z) => ({ id: z, label: z }));
246
- }
247
- catch {
248
- return [{ id: 'UTC', label: 'UTC' }];
249
- }
250
- }, []);
251
239
  // A draft waits on its instructions: they have the focus when it opens.
252
240
  const drafty = Boolean(a?.draft);
253
241
  useEffect(() => {
@@ -350,7 +338,7 @@ export function Editor({ id }) {
350
338
  const says = switchesAlone ? (on ? 'On' : 'Off') : !can ? 'Add instructions first' : on ? 'On when saved' : 'Off when saved';
351
339
  const state = fresh ? 'Not created yet' : dirty ? 'Unsaved changes' : '';
352
340
  const sayId = `${id}-switch`;
353
- 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: () => {
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: () => {
354
342
  setBase(drafted(a));
355
343
  setD(drafted(a));
356
344
  setNote('');
@@ -358,7 +346,7 @@ export function Editor({ id }) {
358
346
  ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
359
347
  : a?.draft
360
348
  ? 'A draft: say what it should do and save it, and it runs.'
361
- : [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.' : 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(Field, { label: "Post to", hint: !host.admin
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
362
350
  ? 'An org admin chooses where automations post: the accounts are the organization’s.'
363
351
  : !d.postTo.length
364
352
  ? 'Turn an account on and each run’s answer is a post for it.'
@@ -376,11 +364,11 @@ export function Editor({ id }) {
376
364
  : `${c.available ? 'Not connected: an org admin connects it in Settings → Integrations.' : p.needs} ${on || d.permissions === 'ask'
377
365
  ? 'Runs hold the post for you to read; posting it fails until it is connected.'
378
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));
379
- }) }), _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.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 () => {
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 () => {
380
368
  await remove(t, a.id);
381
369
  leave({ replace: true });
382
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" })] })] })] })] }));
383
371
  }
384
372
  function Page({ title, says, onBack }) {
385
- 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 })] }) }));
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 })] }) }));
386
374
  }
package/lib/builder.js CHANGED
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  *
5
5
  * / New — the empty state, a composer over the codebase it works on
6
6
  * /sess_… one run, live
7
- * /-/automations work that runs itself; /-/automations/<id> one, to edit and run
7
+ * /-/automations scheduled tasks and what ran; /-/automations/<id> one, to edit and run
8
8
  * /-/projects the org's repositories on the forge: a project is a repository
9
9
  * /-/sync link repositories from GitHub onto the forge
10
10
  * /-/issues the forge's issues
11
- * /-/artifacts what this org has built
11
+ * /-/artifacts the Library: everything you made, in Chat, Dev and Studio
12
12
  * /-/templates the public starters
13
13
  * /-/customize skills, connectors, plugins and agents
14
14
  * /<org>/<repo> a repository's workspace, in this same window
@@ -47,7 +47,8 @@ import { Plans } from './plans.js';
47
47
  import { path, route } from './route.js';
48
48
  import { Run } from './run.js';
49
49
  import { DOTS } from './section.js';
50
- import { Artifacts, Templates } from './shelf.js';
50
+ import { Library } from './library.js';
51
+ import { Templates } from './shelf.js';
51
52
  import { Find } from './find.js';
52
53
  import { Slack, useWho, Who } from './foot.js';
53
54
  import { Settings } from './settings/index.js';
@@ -111,7 +112,7 @@ function Pane({ onStarted }) {
111
112
  return _jsx(Editor, { id: r.id }, r.id);
112
113
  if (r.kind === 'screen') {
113
114
  if (r.screen === 'artifacts')
114
- return _jsx(Artifacts, {});
115
+ return _jsx(Library, {});
115
116
  if (r.screen === 'templates')
116
117
  return _jsx(Templates, {});
117
118
  if (r.screen === 'plans')
package/lib/forge.js CHANGED
@@ -16,7 +16,7 @@ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from
16
16
  import { useEffect, useMemo, useState } from 'react';
17
17
  import { ago } from './ago.js';
18
18
  import { codebases } from './api/codebases.js';
19
- import { Automations } from './automations.js';
19
+ import { Scheduled } from './scheduled.js';
20
20
  import { boards, everything, sources } from './api/work.js';
21
21
  import { boardKey, pinRepo, readPending, writePending } from './choice.js';
22
22
  import { useKept, useRead } from './data.js';
@@ -40,7 +40,7 @@ const STATUS = {
40
40
  };
41
41
  export function Forge({ screen }) {
42
42
  if (screen === 'automations')
43
- return _jsx(Automations, {});
43
+ return _jsx(Scheduled, {});
44
44
  if (screen === 'sync')
45
45
  return _jsx(Sync, {});
46
46
  // The fleet's native servers are Connectors → Discover now; the old address lands there.
package/lib/index.d.ts CHANGED
@@ -18,6 +18,10 @@
18
18
  * which loads in Node without the components.
19
19
  */
20
20
  export { Builder } from './builder.tsx';
21
+ export { Library, Outputs, Preview, Thumb, want } from './library.tsx';
22
+ export { SOURCE, type Item, type Kind as ItemKind, type Source } from './api/library.ts';
23
+ export { Scheduled, schedule } from './scheduled.tsx';
24
+ export { Editor } from './automations.tsx';
21
25
  export { nav, NEW, type Group } from './nav.tsx';
22
26
  export { DevSection, DOTS, useSessions } from './section.tsx';
23
27
  export { HostProvider } from './host.tsx';
package/lib/index.js CHANGED
@@ -18,6 +18,10 @@
18
18
  * which loads in Node without the components.
19
19
  */
20
20
  export { Builder } from './builder.js';
21
+ export { Library, Outputs, Preview, Thumb, want } from './library.js';
22
+ export { SOURCE } from './api/library.js';
23
+ export { Scheduled, schedule } from './scheduled.js';
24
+ export { Editor } from './automations.js';
21
25
  export { nav, NEW } from './nav.js';
22
26
  export { DevSection, DOTS, useSessions } from './section.js';
23
27
  export { HostProvider } from './host.js';
@@ -0,0 +1,21 @@
1
+ import { type DropdownMenuProps } from '@hanzo/ui';
2
+ import { type Item } from './api/library.ts';
3
+ type MenuItems = NonNullable<DropdownMenuProps['items']>;
4
+ /** One square: the still, or the kind's mark on a quiet ground. */
5
+ export declare function Thumb({ x, size: n }: {
6
+ x: Item;
7
+ size?: number;
8
+ }): import("react").JSX.Element;
9
+ /** Open the Library at one item: the move leaves its id for the screen to take. */
10
+ export declare function want(id: string): void;
11
+ /** One item, read fresh so its signed URLs are live: the thing itself, then what it is. */
12
+ export declare function Preview({ id, rows }: {
13
+ id: string;
14
+ rows?: (x: Item) => MenuItems;
15
+ }): import("react").JSX.Element;
16
+ /** A run's outputs as small tiles: each opens in the Library. */
17
+ export declare function Outputs({ ids }: {
18
+ ids: string[];
19
+ }): import("react").JSX.Element | null;
20
+ export declare function Library(): import("react").JSX.Element;
21
+ export {};