@hanzo/build 0.2.57 → 0.2.59
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 +37 -11
- package/lib/activity.d.ts +20 -0
- package/lib/activity.js +38 -0
- package/lib/api/auto.d.ts +77 -2
- package/lib/api/auto.js +137 -4
- package/lib/api/library.d.ts +121 -0
- package/lib/api/library.js +157 -0
- package/lib/automations.d.ts +28 -1
- package/lib/automations.js +49 -61
- package/lib/builder.js +5 -4
- package/lib/forge.js +2 -2
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/library.d.ts +21 -0
- package/lib/library.js +407 -0
- package/lib/nav.js +3 -3
- package/lib/panel.js +11 -3
- package/lib/run.js +17 -1
- package/lib/scheduled.d.ts +3 -0
- package/lib/scheduled.js +281 -0
- package/lib/shelf.d.ts +6 -5
- package/lib/shelf.js +9 -8
- package/package.json +1 -1
package/lib/scheduled.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Scheduled tasks: everything that runs without you watching, in one place —
|
|
4
|
+
* the prompts you scheduled from Chat and Dev, background runs, Studio renders
|
|
5
|
+
* and workflows.
|
|
6
|
+
*
|
|
7
|
+
* -/automations your tasks, then Activity: every run, across products
|
|
8
|
+
* -/automations/<id> one task's editor and runs (automations.tsx)
|
|
9
|
+
*
|
|
10
|
+
* Tasks are /v1/auto/automations and runs are /v1/auto/jobs (api/auto.ts), both
|
|
11
|
+
* the reader's own; /v1/auto/events keeps them live while the page is open, and
|
|
12
|
+
* a read every half minute keeps them right when the stream is down. A new task
|
|
13
|
+
* is said in plain words — "every Monday at 9am" — and the platform says back
|
|
14
|
+
* what it understood before anything is created.
|
|
15
|
+
*/
|
|
16
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
17
|
+
import { Clock, ExternalLink, MoreHorizontal, Pencil, Play, Plus, ScrollText, Trash2 } from '@hanzogui/lucide-icons-2';
|
|
18
|
+
import { Button, DropdownMenu, Input, Switch, Textarea } from '@hanzo/ui';
|
|
19
|
+
import { StatusDot } from '@hanzo/ui/chat';
|
|
20
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
21
|
+
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
22
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
23
|
+
import { automations, create, cron, events, jobs, remove, save, start, words } from './api/auto.js';
|
|
24
|
+
import { Refusal } from './api/call.js';
|
|
25
|
+
import { ENSO, limits as readLimits, models } from './api/models.js';
|
|
26
|
+
import { Confirm, Field, Line, Sheet } from './customize/ui.js';
|
|
27
|
+
import { local, PLAN, shaped, tidy, unplanned, unready, When, zone } from './automations.js';
|
|
28
|
+
import { useRead } from './data.js';
|
|
29
|
+
import { useHost, useTarget } from './host.js';
|
|
30
|
+
import { about, dollars, DOTS, lasted, merged, SAID, titled } from './activity.js';
|
|
31
|
+
import { SOURCE } from './api/library.js';
|
|
32
|
+
import { Outputs } from './library.js';
|
|
33
|
+
import { path } from './route.js';
|
|
34
|
+
/** Where a move into Scheduled leaves the prompt a new task should start from. */
|
|
35
|
+
const ASK = 'hanzo.build.scheduled.ask';
|
|
36
|
+
/** Open Scheduled with a new task started from `prompt`: Dev's "Schedule this". */
|
|
37
|
+
export function schedule(prompt, go) {
|
|
38
|
+
try {
|
|
39
|
+
sessionStorage.setItem(ASK, prompt);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Without storage the sheet opens empty.
|
|
43
|
+
}
|
|
44
|
+
go(path({ kind: 'screen', screen: 'automations' }));
|
|
45
|
+
}
|
|
46
|
+
function asked() {
|
|
47
|
+
try {
|
|
48
|
+
const s = sessionStorage.getItem(ASK) ?? '';
|
|
49
|
+
sessionStorage.removeItem(ASK);
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** /v1/auto/events, followed while signed in: each job and task as it changes, again after a drop. */
|
|
57
|
+
function useLive(on) {
|
|
58
|
+
const host = useHost();
|
|
59
|
+
const t = useTarget();
|
|
60
|
+
const signed = Boolean(host.person);
|
|
61
|
+
const heard = useRef(on);
|
|
62
|
+
heard.current = on;
|
|
63
|
+
const [live, setLive] = useState(false);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!signed)
|
|
66
|
+
return;
|
|
67
|
+
const ctl = new AbortController();
|
|
68
|
+
let wait = 1000;
|
|
69
|
+
void (async () => {
|
|
70
|
+
while (!ctl.signal.aborted) {
|
|
71
|
+
const began = Date.now();
|
|
72
|
+
try {
|
|
73
|
+
await events(t, (h) => heard.current(h.kind === 'job' ? { job: h.job } : { automation: h.automation }), ctl.signal, () => setLive(true));
|
|
74
|
+
// A stream that stayed open a while is the platform's; one that closed at once is waited on longer each time.
|
|
75
|
+
if (Date.now() - began > 5000)
|
|
76
|
+
wait = 1000;
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
// A stream the platform does not serve here is not asked for again; the half-minute read stands in.
|
|
80
|
+
if (e instanceof Refusal && (e.status === 404 || e.status === 401 || e.status === 403))
|
|
81
|
+
return setLive(false);
|
|
82
|
+
}
|
|
83
|
+
setLive(false);
|
|
84
|
+
if (ctl.signal.aborted)
|
|
85
|
+
return;
|
|
86
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
87
|
+
wait = Math.min(wait * 2, 30_000);
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
return () => ctl.abort();
|
|
91
|
+
}, [t, signed]);
|
|
92
|
+
return live;
|
|
93
|
+
}
|
|
94
|
+
export function Scheduled() {
|
|
95
|
+
const host = useHost();
|
|
96
|
+
const t = useTarget();
|
|
97
|
+
const signed = Boolean(host.person);
|
|
98
|
+
const [tick, setTick] = useState(0);
|
|
99
|
+
const tasks = useRead(signed ? () => automations(t) : null, [], [t, signed, tick]);
|
|
100
|
+
const runs = useRead(signed ? () => jobs(t, { limit: 50 }) : null, [], [t, signed, tick]);
|
|
101
|
+
const [shownTasks, setTasks] = useState([]);
|
|
102
|
+
const [shownRuns, setRuns] = useState([]);
|
|
103
|
+
useEffect(() => setTasks(tasks.value), [tasks.value]);
|
|
104
|
+
useEffect(() => setRuns(runs.value), [runs.value]);
|
|
105
|
+
const [problem, setProblem] = useState('');
|
|
106
|
+
const [making, setMaking] = useState(null);
|
|
107
|
+
const [deleting, setDeleting] = useState(null);
|
|
108
|
+
const [looking, setLooking] = useState(null);
|
|
109
|
+
const [now, setNow] = useState(() => Date.now());
|
|
110
|
+
// Dev's "Schedule this" arrives with the prompt the sheet starts from.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const prompt = asked();
|
|
113
|
+
if (prompt)
|
|
114
|
+
setMaking(prompt);
|
|
115
|
+
}, []);
|
|
116
|
+
const live = useLive(({ job, automation }) => {
|
|
117
|
+
if (job) {
|
|
118
|
+
setRuns((was) => merged(was, job));
|
|
119
|
+
setLooking((was) => (was && was.id === job.id ? job : was));
|
|
120
|
+
}
|
|
121
|
+
if (automation)
|
|
122
|
+
setTasks((was) => (was.some((a) => a.id === automation.id) ? was.map((a) => (a.id === automation.id ? automation : a)) : [automation, ...was]));
|
|
123
|
+
});
|
|
124
|
+
// Without the stream, a read every half minute; with it, a clock for the running durations.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!signed)
|
|
127
|
+
return;
|
|
128
|
+
const id = setInterval(() => (live ? setNow(Date.now()) : setTick((n) => n + 1)), live ? 1000 : 30_000);
|
|
129
|
+
return () => clearInterval(id);
|
|
130
|
+
}, [signed, live]);
|
|
131
|
+
const act = async (what, work) => {
|
|
132
|
+
setProblem('');
|
|
133
|
+
try {
|
|
134
|
+
await work();
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
setProblem(unplanned(e) ? PLAN : `${what}: ${e.message}`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const flip = (a, on) => act(a.name, async () => {
|
|
141
|
+
const next = await save(t, a.id, { enabled: on });
|
|
142
|
+
setTasks((was) => was.map((x) => (x.id === next.id ? next : x)));
|
|
143
|
+
});
|
|
144
|
+
const run = (a) => act(a.name, async () => {
|
|
145
|
+
const r = await start(t, a.id);
|
|
146
|
+
if (r.status === 'skipped')
|
|
147
|
+
setProblem(`${a.name}: not started, the previous run is still going.`);
|
|
148
|
+
setTick((n) => n + 1);
|
|
149
|
+
});
|
|
150
|
+
const edit = (id) => host.go(path({ kind: 'automation', id }));
|
|
151
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 880, mx: "auto", gap: "$5", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { render: "h1", size: "$7", fontWeight: "600", color: "$ink", children: "Scheduled tasks" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "What runs without you watching: prompts on a schedule, background runs, Studio renders and workflows." })] }), _jsxs(Button, { size: "sm", shrink: 0, disabled: !signed, onPress: () => setMaking(''), children: [_jsx(Plus, { size: 14 }), "New task"] })] }), _jsx(Line, { children: problem }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { render: "h2", size: "$3", color: "$ink", fontWeight: "500", children: "Tasks" }), !signed ? (_jsx(Said, { says: "Sign in to see your scheduled tasks.", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) })) : tasks.error && !shownTasks.length ? (_jsx(Said, { says: tasks.error.message })) : tasks.loading && !shownTasks.length ? (_jsx(Said, { says: "Reading your tasks\u2026" })) : !shownTasks.length ? (_jsx(Said, { says: "No scheduled tasks yet. Ask Chat or Dev to do something on a schedule, or start one here.", children: _jsx(Button, { size: "sm", onPress: () => setMaking(''), children: "New task" }) })) : (_jsx(YStack, { role: "list", "aria-label": "Your tasks", borderWidth: 1, borderColor: "$borderColor", rounded: "$4", overflow: "hidden", children: shownTasks.map((a, i) => (_jsx(Task, { a: a, first: i === 0, onOpen: () => edit(a.id), onSwitch: (on) => void flip(a, on), onRun: () => void run(a), onDelete: () => setDeleting(a) }, a.id))) }))] }), signed ? (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { render: "h2", size: "$3", color: "$ink", fontWeight: "500", flex: 1, children: "Activity" }), _jsx(SizableText, { size: "$1", color: "$soft", children: live ? 'Live' : '' })] }), runs.error && !shownRuns.length ? (_jsx(Said, { says: runs.error.message })) : runs.loading && !shownRuns.length ? (_jsx(Said, { says: "Reading what ran\u2026" })) : !shownRuns.length ? (_jsx(Said, { says: "Nothing has run yet. A task's runs, Dev's background runs and Studio's renders show here as they go." })) : (_jsx(YStack, { role: "list", "aria-label": "Activity", borderWidth: 1, borderColor: "$borderColor", rounded: "$4", overflow: "hidden", children: shownRuns.map((j, i) => (_jsx(Ran, { j: j, first: i === 0, now: now, onOpen: () => setLooking(j) }, j.id))) }))] })) : null] }), _jsx(Make, { prompt: making, onClose: () => setMaking(null), onMade: (a) => {
|
|
152
|
+
setTasks((was) => [a, ...was.filter((x) => x.id !== a.id)]);
|
|
153
|
+
setProblem(`Scheduled: ${a.name}.`);
|
|
154
|
+
} }), _jsx(Detail, { j: looking, onClose: () => setLooking(null), now: now }), deleting ? (_jsx(Confirm, { what: deleting.name, says: "It stops running, and its schedule and its runs go with it. A run going now is stopped.", open: true, onOpenChange: (o) => !o && setDeleting(null), onYes: async () => {
|
|
155
|
+
await remove(t, deleting.id);
|
|
156
|
+
setTasks((was) => was.filter((x) => x.id !== deleting.id));
|
|
157
|
+
setDeleting(null);
|
|
158
|
+
} })) : null] }));
|
|
159
|
+
}
|
|
160
|
+
/** One task: what it is and when it runs next, its switch, and its menu. */
|
|
161
|
+
function Task({ a, first, onOpen, onSwitch, onRun, onDelete }) {
|
|
162
|
+
const when = a.draft
|
|
163
|
+
? 'Needs instructions'
|
|
164
|
+
: [words(a.schedule), a.enabled && a.next ? `next ${local(a.next)}` : a.schedule.kind !== 'manual' && !a.enabled ? 'paused' : ''].filter(Boolean).join(' · ');
|
|
165
|
+
return (_jsxs(XStack, { role: "listitem", items: "center", gap: "$3", px: "$3", py: "$3", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${a.name}`, onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", cursor: "pointer", style: { textAlign: 'left' }, children: [_jsx(YStack, { width: 32, height: 32, rounded: "$3", bg: "$raised", items: "center", justify: "center", shrink: 0, "aria-hidden": true, children: _jsx(Clock, { size: 16 }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, shrink: 1, children: a.name }), _jsx(Badge, { children: SOURCE[a.source] }), a.draft ? _jsx(Badge, { children: "Draft" }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: when })] })] }), _jsx(Switch, { checked: a.enabled, disabled: a.draft, onCheckedChange: onSwitch, "aria-label": `${a.name} on` }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${a.name}`, width: 30, height: 30, items: "center", justify: "center", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
166
|
+
...(a.draft ? [] : [{ key: 'run', label: 'Run now', icon: _jsx(Play, { size: 16 }), onSelect: onRun }]),
|
|
167
|
+
{ key: 'edit', label: 'Edit', icon: _jsx(Pencil, { size: 16 }), onSelect: onOpen },
|
|
168
|
+
{ key: 'delete', label: 'Delete', icon: _jsx(Trash2, { size: 16 }), onSelect: onDelete },
|
|
169
|
+
] })] }));
|
|
170
|
+
}
|
|
171
|
+
/** One run in Activity. */
|
|
172
|
+
function Ran({ j, first, now, onOpen }) {
|
|
173
|
+
return (_jsx(XStack, { role: "listitem", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { render: "button", "aria-label": `${j.title}: ${SAID[j.state]}`, onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [_jsx(StatusDot, { status: DOTS[j.state] }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: j.title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: about(j, now) })] }), _jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: j.state === 'running' && j.progress !== null ? `${Math.round(j.progress * 100)}%` : SAID[j.state] })] }) }));
|
|
174
|
+
}
|
|
175
|
+
/** One run, opened: how it went, its log, what it made and what it cost. */
|
|
176
|
+
function Detail({ j, onClose, now }) {
|
|
177
|
+
const host = useHost();
|
|
178
|
+
return (_jsx(Sheet, { title: j?.title ?? 'Run', open: Boolean(j), onOpenChange: (o) => !o && onClose(), width: 520, children: j ? (_jsxs(YStack, { gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusDot, { status: DOTS[j.state] }), _jsxs(SizableText, { size: "$2", color: "$ink", children: [SAID[j.state], j.status && j.status !== j.state ? ` (${j.status})` : ''] })] }), _jsx(Facts, { rows: [
|
|
179
|
+
['From', SOURCE[j.product]],
|
|
180
|
+
['Started', j.started ? local(j.started) : '—'],
|
|
181
|
+
['Took', lasted(j.started, j.finished, now) || '—'],
|
|
182
|
+
['Cost', j.cost !== null ? dollars(j.cost) : 'Not known yet'],
|
|
183
|
+
] }), j.error ? (_jsx(SizableText, { size: "$2", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: j.error })) : null, j.summary ? (_jsx(SizableText, { size: "$2", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: j.summary })) : null, j.outputs.length ? (_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "What it made" }), _jsx(Outputs, { ids: j.outputs })] })) : null, _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [j.session ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.go(j.session), children: [_jsx(ScrollText, { size: 14 }), "Open its log"] })) : null, j.origin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.open(j.origin), children: [_jsx(ExternalLink, { size: 14 }), "Open where it runs"] })) : null, j.automation ? (_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => host.go(path({ kind: 'automation', id: j.automation })), children: [_jsx(Pencil, { size: 14 }), "Edit the task"] })) : null] })] })) : null }));
|
|
184
|
+
}
|
|
185
|
+
/** A new task: what it does, when in plain words, and the model it thinks with. */
|
|
186
|
+
function Make({ prompt, onClose, onMade }) {
|
|
187
|
+
const host = useHost();
|
|
188
|
+
const t = useTarget();
|
|
189
|
+
const signed = Boolean(host.person);
|
|
190
|
+
const open = prompt !== null;
|
|
191
|
+
const [name, setName] = useState('');
|
|
192
|
+
const [asks, setAsks] = useState('');
|
|
193
|
+
const [said, setSaid] = useState('');
|
|
194
|
+
const [read, setRead] = useState(null);
|
|
195
|
+
const [reading, setReading] = useState(false);
|
|
196
|
+
const [unread, setUnread] = useState('');
|
|
197
|
+
const [picking, setPicking] = useState(false);
|
|
198
|
+
const [picked, setPicked] = useState(() => shaped('daily', { kind: 'manual' }));
|
|
199
|
+
const [model, setModel] = useState(null);
|
|
200
|
+
const [busy, setBusy] = useState(false);
|
|
201
|
+
const [note, setNote] = useState('');
|
|
202
|
+
const catalog = useRead(signed && open ? () => models(t) : null, [], [t, signed, open]);
|
|
203
|
+
const { limits } = useLimits(signed && open ? (signal) => readLimits(t, signal) : null, t);
|
|
204
|
+
// Each opening starts from what it was handed.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (prompt === null)
|
|
207
|
+
return;
|
|
208
|
+
setAsks(prompt);
|
|
209
|
+
setName(titled(prompt));
|
|
210
|
+
setSaid('');
|
|
211
|
+
setRead(null);
|
|
212
|
+
setUnread('');
|
|
213
|
+
setPicking(false);
|
|
214
|
+
setNote('');
|
|
215
|
+
}, [prompt]);
|
|
216
|
+
// The words are read once the typing settles; the answer is said back before anything is made.
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const text = said.trim();
|
|
219
|
+
if (!open || !text || picking) {
|
|
220
|
+
setRead(null);
|
|
221
|
+
setUnread('');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const ctl = new AbortController();
|
|
225
|
+
const id = setTimeout(() => {
|
|
226
|
+
setReading(true);
|
|
227
|
+
cron(t, text, zone(), { signal: ctl.signal })
|
|
228
|
+
.then((p) => {
|
|
229
|
+
setRead(p);
|
|
230
|
+
setUnread('');
|
|
231
|
+
})
|
|
232
|
+
.catch((e) => {
|
|
233
|
+
if (ctl.signal.aborted)
|
|
234
|
+
return;
|
|
235
|
+
setRead(null);
|
|
236
|
+
setUnread(e.message);
|
|
237
|
+
})
|
|
238
|
+
.finally(() => !ctl.signal.aborted && setReading(false));
|
|
239
|
+
}, 450);
|
|
240
|
+
return () => {
|
|
241
|
+
clearTimeout(id);
|
|
242
|
+
ctl.abort();
|
|
243
|
+
};
|
|
244
|
+
}, [said, open, picking, t]);
|
|
245
|
+
const when = picking ? picked : (read?.schedule ?? null);
|
|
246
|
+
const wrong = when ? unready(when) : '';
|
|
247
|
+
const make = async () => {
|
|
248
|
+
if (!name.trim())
|
|
249
|
+
return setNote('Name it first.');
|
|
250
|
+
if (!asks.trim())
|
|
251
|
+
return setNote('Say what it should do.');
|
|
252
|
+
if (!when)
|
|
253
|
+
return setNote(said.trim() ? 'Say when in other words, or choose instead.' : 'Say when it runs.');
|
|
254
|
+
if (wrong)
|
|
255
|
+
return setNote(wrong);
|
|
256
|
+
setBusy(true);
|
|
257
|
+
setNote('');
|
|
258
|
+
try {
|
|
259
|
+
const d = { name: name.trim(), instructions: asks.trim(), model, schedule: when, permissions: 'ask', notify: true, postTo: [], enabled: true, source: 'dev' };
|
|
260
|
+
onMade(await create(t, tidy(d)));
|
|
261
|
+
onClose();
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
setNote(unplanned(e) ? PLAN : e.message);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
setBusy(false);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const heard = useMemo(() => (read ? [read.says || words(read.schedule), read.next ? `next ${local(read.next)}` : ''].filter(Boolean).join(' · ') : ''), [read]);
|
|
271
|
+
return (_jsx(Sheet, { title: "New task", open: open, onOpenChange: (o) => !o && !busy && onClose(), width: 560, children: _jsxs(YStack, { gap: "$4", children: [_jsx(Field, { label: "Name", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "Monday summary", "aria-label": "Task name" }) }), _jsx(Field, { label: "What it does", hint: "Each run is one Dev run in this organization, as you. It reads and proposes; it changes nothing on its own.", children: _jsx(Textarea, { value: asks, onChangeText: setAsks, placeholder: "Summarize what changed in my repositories this week, and what needs me.", "aria-label": "What it does", rows: 5 }) }), _jsx(Field, { label: "When", hint: picking ? undefined : 'In your own words: every weekday at 8am, every 5 minutes, first of the month.', children: picking ? (_jsxs(YStack, { gap: "$2", children: [_jsx(When, { s: picked, onChange: setPicked }), _jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setPicking(false), children: "Say it instead" }) })] })) : (_jsxs(YStack, { gap: "$2", children: [_jsx(Input, { value: said, onChangeText: setSaid, placeholder: "Every Monday at 9am", "aria-label": "When it runs" }), _jsx(SizableText, { size: "$1", color: unread ? '$soft' : '$ink', role: "status", minH: 18, children: reading ? 'Reading…' : unread ? `${unread} Choose instead, or say it another way.` : heard }), _jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setPicking(true), children: "Choose instead" }) })] })) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: model ?? ENSO, onChange: (id) => setModel(id === ENSO ? null : id), loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: busy, onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !signed, onPress: () => void make(), children: busy ? 'Creating…' : 'Create' })] })] }) }));
|
|
272
|
+
}
|
|
273
|
+
function Badge({ children }) {
|
|
274
|
+
return (_jsx(SizableText, { size: "$1", color: "$soft", px: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: children }));
|
|
275
|
+
}
|
|
276
|
+
function Facts({ rows }) {
|
|
277
|
+
return (_jsx(YStack, { gap: "$1", children: rows.map(([k, v]) => (_jsxs(XStack, { gap: "$3", children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 72, children: k }), _jsx(SizableText, { size: "$1", color: "$ink", flex: 1, children: v })] }, k))) }));
|
|
278
|
+
}
|
|
279
|
+
function Said({ says, children }) {
|
|
280
|
+
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] }));
|
|
281
|
+
}
|
package/lib/shelf.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What this org has
|
|
3
|
-
* every
|
|
4
|
-
* private, and its rename, visibility and delete. A
|
|
5
|
-
*
|
|
2
|
+
* What this org has deployed: a few starters to make something new from, then
|
|
3
|
+
* every site under the month it was last edited, whether it is public or
|
|
4
|
+
* private, and its rename, visibility and delete. A site belongs to the org and
|
|
5
|
+
* names no author, so the list does not split yours from a colleague's — which
|
|
6
|
+
* is why it is a section of its own under the Library's items, which are yours.
|
|
6
7
|
*/
|
|
7
|
-
export declare function
|
|
8
|
+
export declare function Sites(): import("react").JSX.Element;
|
|
8
9
|
export declare function Templates(): import("react").JSX.Element;
|
package/lib/shelf.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* public starters (Templates).
|
|
3
|
+
* Two shelves: the sites this org has deployed (Sites, drawn at the foot of the
|
|
4
|
+
* Library — library.tsx), and the public starters (Templates).
|
|
5
5
|
*
|
|
6
6
|
* A project is a real row — `/v1/projects`, the same one a deployed site is
|
|
7
7
|
* served from — so opening one opens its workspace at its own address. A
|
|
@@ -70,12 +70,13 @@ function edited(s) {
|
|
|
70
70
|
return `Edited ${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year })}`;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* What this org has
|
|
74
|
-
* every
|
|
75
|
-
* private, and its rename, visibility and delete. A
|
|
76
|
-
*
|
|
73
|
+
* What this org has deployed: a few starters to make something new from, then
|
|
74
|
+
* every site under the month it was last edited, whether it is public or
|
|
75
|
+
* private, and its rename, visibility and delete. A site belongs to the org and
|
|
76
|
+
* names no author, so the list does not split yours from a colleague's — which
|
|
77
|
+
* is why it is a section of its own under the Library's items, which are yours.
|
|
77
78
|
*/
|
|
78
|
-
export function
|
|
79
|
+
export function Sites() {
|
|
79
80
|
const t = useTarget();
|
|
80
81
|
const host = useHost();
|
|
81
82
|
const list = useProjects(t, Boolean(host.person));
|
|
@@ -97,7 +98,7 @@ export function Artifacts() {
|
|
|
97
98
|
}
|
|
98
99
|
};
|
|
99
100
|
const said = failed || note;
|
|
100
|
-
return (_jsxs(YStack, {
|
|
101
|
+
return (_jsxs(YStack, { width: "100%", role: "region", "aria-label": "Sites", children: [_jsxs(YStack, { width: "100%", children: [_jsxs(YStack, { gap: "$1", pb: "$4", children: [_jsx(SizableText, { render: "h2", size: "$5", color: "$ink", children: "Sites" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "What this organization has built. Open one to keep building it." })] }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Make something new" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsx(YStack, { px: "$3", py: "$2.5", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: busy === x.slug ? 'Copying…' : x.title }) })] }, x.slug))) })] })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "New run" })] })) : (_jsx(YStack, { gap: "$5", children: months(list.value).map((g) => (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: g.label }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: g.rows.map((p, i) => (_jsxs(XStack, { items: "center", pr: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { flex: 1, minW: 0, render: "button", "aria-label": `Open ${p.name}`, onPress: () => host.go(p.slug), items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [p.visibility === 'private' ? _jsx(Lock, { size: 16, opacity: 0.7 }) : _jsx(Globe, { size: 16, opacity: 0.7 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [p.visibility === 'private' ? 'Private' : 'Public', edited(when(p)), p.live ? new URL(p.live).host : 'Not deployed yet']
|
|
101
102
|
.filter(Boolean)
|
|
102
103
|
.join(' · ') })] })] }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${p.name}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$raised' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
103
104
|
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(p) },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.59",
|
|
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.",
|