@hanzo/build 0.2.41 → 0.2.43
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 +11 -0
- package/lib/api/auto.js +10 -0
- package/lib/api/models.d.ts +15 -18
- package/lib/api/models.js +11 -41
- package/lib/automations.js +78 -38
- package/lib/customize/agents.js +3 -12
- package/lib/landing.js +8 -4
- package/lib/pick.d.ts +2 -0
- package/lib/pick.js +21 -0
- package/lib/run.js +8 -5
- package/lib/settings/code.js +4 -3
- package/package.json +4 -4
- package/lib/access.d.ts +0 -4
- package/lib/access.js +0 -16
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
|
+
}
|
package/lib/api/models.d.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import { type Target } from './call.ts';
|
|
2
|
-
export interface Model {
|
|
3
|
-
id: string;
|
|
4
|
-
label: string;
|
|
5
|
-
/** Listed and never chosen. */
|
|
6
|
-
disabled?: boolean;
|
|
7
|
-
hint?: string;
|
|
8
|
-
/** Where a person asks for access to a model they cannot call. */
|
|
9
|
-
request?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare const ENSO = "enso";
|
|
12
|
-
/** A readable name for an id: `anthropic/claude-opus-5.5` → `Claude Opus 5.5`, `zen5-coder` → `Zen5 Coder`. */
|
|
13
|
-
export declare function label(id: string): string;
|
|
14
|
-
export declare function models(t: Target): Promise<Model[]>;
|
|
15
1
|
/**
|
|
16
|
-
* The models
|
|
17
|
-
*
|
|
18
|
-
*
|
|
2
|
+
* The models a run can be routed through, from the platform's own catalog.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/models → {data: [{id, class, family, …}]}
|
|
5
|
+
*
|
|
6
|
+
* Read by `@hanzo/ui/models` `parseModels`, so a model's class and family are
|
|
7
|
+
* the gateway's fields, never guessed from its id. `ENSO` is the router: it
|
|
8
|
+
* picks a model per step, which is why it is the default, and a run that names
|
|
9
|
+
* it names no model at all.
|
|
19
10
|
*/
|
|
20
|
-
|
|
11
|
+
import { type ModelCatalogEntry } from '@hanzo/ui/models/catalog';
|
|
12
|
+
import { type Target } from './call.ts';
|
|
13
|
+
export { ENSO } from '@hanzo/ui/models/catalog';
|
|
14
|
+
export type Model = ModelCatalogEntry;
|
|
15
|
+
export declare function models(t: Target): Promise<Model[]>;
|
|
16
|
+
/** The payer's plan usage, as `GET /v1/ai/limits` answers it: what a picker marks paused. */
|
|
17
|
+
export declare function limits(t: Target, signal?: AbortSignal): Promise<unknown>;
|
package/lib/api/models.js
CHANGED
|
@@ -1,50 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The models a run can be routed through, from the platform's own catalog.
|
|
3
3
|
*
|
|
4
|
-
* GET /v1/models → {data: [{id, …}]}
|
|
4
|
+
* GET /v1/models → {data: [{id, class, family, …}]}
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @hanzo/ui's `RESEARCH`: listed so a person knows they exist, disabled so no
|
|
11
|
-
* run is ever started on one, with where to ask for access.
|
|
6
|
+
* Read by `@hanzo/ui/models` `parseModels`, so a model's class and family are
|
|
7
|
+
* the gateway's fields, never guessed from its id. `ENSO` is the router: it
|
|
8
|
+
* picks a model per step, which is why it is the default, and a run that names
|
|
9
|
+
* it names no model at all.
|
|
12
10
|
*/
|
|
13
|
-
import {
|
|
11
|
+
import { parseModels } from '@hanzo/ui/models/catalog';
|
|
14
12
|
import { call } from './call.js';
|
|
15
|
-
export
|
|
16
|
-
/** A readable name for an id: `anthropic/claude-opus-5.5` → `Claude Opus 5.5`, `zen5-coder` → `Zen5 Coder`. */
|
|
17
|
-
export function label(id) {
|
|
18
|
-
const tail = id.slice(id.lastIndexOf('/') + 1).replace(/:.*$/, '');
|
|
19
|
-
return tail
|
|
20
|
-
.split('-')
|
|
21
|
-
.filter(Boolean)
|
|
22
|
-
.map((w) => (/^\d/.test(w) ? w : w[0].toUpperCase() + w.slice(1)))
|
|
23
|
-
.join(' ');
|
|
24
|
-
}
|
|
13
|
+
export { ENSO } from '@hanzo/ui/models/catalog';
|
|
25
14
|
export async function models(t) {
|
|
26
|
-
|
|
27
|
-
const rows = Array.isArray(raw?.data) ? raw.data : [];
|
|
28
|
-
const ids = rows
|
|
29
|
-
.map((r) => (r && typeof r === 'object' && typeof r.id === 'string' ? r.id : ''))
|
|
30
|
-
.filter((id) => id && !id.endsWith(':batch'));
|
|
31
|
-
const out = [...new Set([ENSO, ...ids])];
|
|
32
|
-
return [
|
|
33
|
-
...out.map((id) => ({ id, label: label(id) })),
|
|
34
|
-
...RESEARCH.filter((m) => !out.includes(m.id)).map((m) => ({
|
|
35
|
-
id: m.id,
|
|
36
|
-
label: m.label ?? label(m.id),
|
|
37
|
-
disabled: true,
|
|
38
|
-
hint: 'Research preview',
|
|
39
|
-
request: m.request,
|
|
40
|
-
})),
|
|
41
|
-
];
|
|
15
|
+
return parseModels(await call(t, 'GET', '/v1/models'));
|
|
42
16
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* embedding, guard, rerank, speech or vision models, which hold no conversation.
|
|
47
|
-
*/
|
|
48
|
-
export function skus(list) {
|
|
49
|
-
return list.filter((m) => /^(enso|zen\d*|free)(-[a-z0-9]+)*$/.test(m.id) && !/-(embedding|guard|rerank|scribe|vl|voice)\b/.test(m.id));
|
|
17
|
+
/** The payer's plan usage, as `GET /v1/ai/limits` answers it: what a picker marks paused. */
|
|
18
|
+
export function limits(t, signal) {
|
|
19
|
+
return call(t, 'GET', '/v1/ai/limits', undefined, { signal });
|
|
50
20
|
}
|
package/lib/automations.js
CHANGED
|
@@ -15,14 +15,15 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
|
15
15
|
import { ChevronLeft, Plus, Workflow } from '@hanzogui/lucide-icons-2';
|
|
16
16
|
import { Button, Input, Switch, Textarea } from '@hanzo/ui';
|
|
17
17
|
import { StatusDot } from '@hanzo/ui/chat';
|
|
18
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
18
19
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
20
|
+
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
19
21
|
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
20
22
|
import { ago } from './ago.js';
|
|
21
|
-
import {
|
|
22
|
-
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';
|
|
23
24
|
import { Refusal } from './api/call.js';
|
|
24
|
-
import { ENSO,
|
|
25
|
-
import { Choice, Confirm, Field, Line } from './customize/ui.js';
|
|
25
|
+
import { ENSO, limits as readLimits, models } from './api/models.js';
|
|
26
|
+
import { Choice, Confirm, Field, Line, Sheet } from './customize/ui.js';
|
|
26
27
|
import { useRead } from './data.js';
|
|
27
28
|
import { useHost, useTarget } from './host.js';
|
|
28
29
|
import { path } from './route.js';
|
|
@@ -117,12 +118,9 @@ function rebase(cur, was, next) {
|
|
|
117
118
|
}
|
|
118
119
|
return out;
|
|
119
120
|
}
|
|
120
|
-
/** A
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return `${e.message || 'This organization is out of credit.'} Add credit in Settings → Billing.`;
|
|
124
|
-
return e.message;
|
|
125
|
-
}
|
|
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.';
|
|
126
124
|
export function Automations() {
|
|
127
125
|
const host = useHost();
|
|
128
126
|
const t = useTarget();
|
|
@@ -140,7 +138,7 @@ export function Automations() {
|
|
|
140
138
|
list.reload();
|
|
141
139
|
}
|
|
142
140
|
catch (e) {
|
|
143
|
-
setProblem(`${a.name}: ${
|
|
141
|
+
setProblem(`${a.name}: ${e.message}`);
|
|
144
142
|
}
|
|
145
143
|
};
|
|
146
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)))] }))] }) }));
|
|
@@ -166,6 +164,7 @@ export function Editor({ id }) {
|
|
|
166
164
|
const saved = useRead(signed && !fresh ? () => automation(t, id) : null, null, [t, signed, id, tick]);
|
|
167
165
|
const history = useRead(signed && !fresh ? () => runs(t, id) : null, [], [t, signed, id, beat]);
|
|
168
166
|
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
167
|
+
const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
|
|
169
168
|
// `base` is what the form started from; a save sends what changed since, and only that.
|
|
170
169
|
const [base, setBase] = useState(BLANK);
|
|
171
170
|
const [d, setD] = useState(BLANK);
|
|
@@ -173,6 +172,11 @@ export function Editor({ id }) {
|
|
|
173
172
|
const [busy, setBusy] = useState('');
|
|
174
173
|
const [note, setNote] = useState('');
|
|
175
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]);
|
|
176
180
|
const here = useRef(true);
|
|
177
181
|
useEffect(() => {
|
|
178
182
|
here.current = true;
|
|
@@ -186,7 +190,7 @@ export function Editor({ id }) {
|
|
|
186
190
|
const change = changes(base, d);
|
|
187
191
|
if (switchesAlone)
|
|
188
192
|
delete change.enabled;
|
|
189
|
-
const dirty =
|
|
193
|
+
const dirty = Object.keys(change).length > 0;
|
|
190
194
|
// What is saved now becomes the form's start, unless the person is mid-edit: then
|
|
191
195
|
// their edit stands, and what they did not touch is not sent back over it.
|
|
192
196
|
const clean = useRef(true);
|
|
@@ -209,15 +213,14 @@ export function Editor({ id }) {
|
|
|
209
213
|
const timer = setInterval(() => setBeat((n) => n + 1), every);
|
|
210
214
|
return () => clearInterval(timer);
|
|
211
215
|
}, [every]);
|
|
212
|
-
|
|
213
|
-
const
|
|
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 } });
|
|
214
223
|
const wrong = unready(d.schedule);
|
|
215
|
-
const items = useMemo(() => {
|
|
216
|
-
const list = skus(catalog.value).filter((m) => m.id !== ENSO);
|
|
217
|
-
const kept = d.model && !list.some((m) => m.id === d.model) ? [{ id: d.model, label: named(d.model) }] : [];
|
|
218
|
-
return [{ id: ENSO, label: 'Enso', hint: 'picks a model for each step' }, ...kept, ...list];
|
|
219
|
-
}, [catalog.value, d.model]);
|
|
220
|
-
const chosen = items.find((m) => m.id === (d.model ?? ENSO)) ?? items[0];
|
|
221
224
|
const zones = useMemo(() => {
|
|
222
225
|
try {
|
|
223
226
|
return ['UTC', ...Intl.supportedValuesOf('timeZone').filter((z) => z !== 'UTC')].map((z) => ({ id: z, label: z }));
|
|
@@ -226,19 +229,43 @@ export function Editor({ id }) {
|
|
|
226
229
|
return [{ id: 'UTC', label: 'UTC' }];
|
|
227
230
|
}
|
|
228
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]);
|
|
229
238
|
const act = async (what, work) => {
|
|
230
239
|
setBusy(what);
|
|
231
240
|
setNote('');
|
|
241
|
+
setCredit(false);
|
|
232
242
|
try {
|
|
233
243
|
await work();
|
|
234
244
|
}
|
|
235
245
|
catch (e) {
|
|
236
|
-
|
|
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);
|
|
237
250
|
}
|
|
238
251
|
finally {
|
|
239
252
|
setBusy('');
|
|
240
253
|
}
|
|
241
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
|
+
};
|
|
242
269
|
const keep = () => act('save', async () => {
|
|
243
270
|
if (fresh) {
|
|
244
271
|
const made = await create(t, tidy(d));
|
|
@@ -247,14 +274,14 @@ export function Editor({ id }) {
|
|
|
247
274
|
host.go(path({ kind: 'automation', id: made.id }), { replace: true });
|
|
248
275
|
return;
|
|
249
276
|
}
|
|
250
|
-
const
|
|
251
|
-
const send = changes(base,
|
|
277
|
+
const snap = d;
|
|
278
|
+
const send = changes(base, tidy(snap));
|
|
252
279
|
if (switchesAlone)
|
|
253
280
|
delete send.enabled;
|
|
254
281
|
const now = Object.keys(send).length ? await save(t, id, send) : a;
|
|
255
282
|
// What was typed while the save was in flight stands.
|
|
256
283
|
setBase(drafted(now));
|
|
257
|
-
setD((cur) => rebase(cur,
|
|
284
|
+
setD((cur) => rebase(cur, snap, drafted(now)));
|
|
258
285
|
setTick((n) => n + 1);
|
|
259
286
|
setNote('Saved.');
|
|
260
287
|
});
|
|
@@ -272,26 +299,39 @@ export function Editor({ id }) {
|
|
|
272
299
|
if (run.status === 'skipped')
|
|
273
300
|
setNote('Not started: the previous run is still going.');
|
|
274
301
|
});
|
|
275
|
-
const
|
|
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
|
+
};
|
|
276
310
|
if (!signed)
|
|
277
|
-
return _jsx(Page, { title: "Automation", onBack: () =>
|
|
311
|
+
return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Sign in to see this organization's automations." });
|
|
278
312
|
if (!fresh && saved.error && !a)
|
|
279
|
-
return _jsx(Page, { title: "Automation", onBack: () =>
|
|
313
|
+
return _jsx(Page, { title: "Automation", onBack: () => leave(), says: saved.error.message });
|
|
280
314
|
if (!ready)
|
|
281
|
-
return _jsx(Page, { title: "Automation", onBack: () =>
|
|
315
|
+
return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Reading the automation\u2026" });
|
|
282
316
|
const s = d.schedule;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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: () => {
|
|
289
324
|
setBase(drafted(a));
|
|
290
325
|
setD(drafted(a));
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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" })] })] })] })] }));
|
|
295
335
|
}
|
|
296
336
|
function Page({ title, says, onBack }) {
|
|
297
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 })] }) }));
|
package/lib/customize/agents.js
CHANGED
|
@@ -13,17 +13,15 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
13
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
14
|
import { Bot } from '@hanzogui/lucide-icons-2';
|
|
15
15
|
import { Button, Input, Switch, Textarea } from '@hanzo/ui';
|
|
16
|
-
import {
|
|
16
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
17
17
|
import { useEffect, useMemo, useState } from 'react';
|
|
18
18
|
import { agents, ALL, create, draft, EMPTY, fromPreset, one, PERIODS, presets, remove, update } from '../api/agents.js';
|
|
19
|
-
import {
|
|
20
|
-
import { label as named, models } from '../api/models.js';
|
|
19
|
+
import { models } from '../api/models.js';
|
|
21
20
|
import { tools } from '../api/tools.js';
|
|
22
21
|
import { useRead } from '../data.js';
|
|
23
22
|
import { useHost, useTarget } from '../host.js';
|
|
24
23
|
import { Add, Choice, Confirm, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile, Visitor } from './ui.js';
|
|
25
24
|
/** The model a new agent runs on when none is chosen: the deployment's own default. */
|
|
26
|
-
const DEFAULT = 'default';
|
|
27
25
|
const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')}`;
|
|
28
26
|
export function Agents({ view, q, adding, onAdding, onView }) {
|
|
29
27
|
const host = useHost();
|
|
@@ -96,13 +94,6 @@ function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
|
|
|
96
94
|
return [...kept, ...listed];
|
|
97
95
|
}, [on.value, d.tools]);
|
|
98
96
|
const shownTools = choices.filter((x) => matches(find, x.name, x.source, x.description));
|
|
99
|
-
const items = useMemo(() => {
|
|
100
|
-
const list = [{ id: DEFAULT, label: 'Default', hint: 'the deployment’s' }, ...catalog.value];
|
|
101
|
-
if (d.model && !list.some((m) => m.id === d.model))
|
|
102
|
-
list.splice(1, 0, { id: d.model, label: named(d.model) });
|
|
103
|
-
return agent ? list.filter((m) => m.id !== DEFAULT) : list;
|
|
104
|
-
}, [catalog.value, d.model, agent]);
|
|
105
|
-
const chosen = items.find((m) => m.id === (d.model || DEFAULT)) ?? null;
|
|
106
97
|
const save = async () => {
|
|
107
98
|
setWorking(true);
|
|
108
99
|
setNote('');
|
|
@@ -119,7 +110,7 @@ function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
|
|
|
119
110
|
}
|
|
120
111
|
};
|
|
121
112
|
const pick = (name, yes) => set({ tools: yes ? [...d.tools, name] : d.tools.filter((n) => n !== name) });
|
|
122
|
-
return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "helper", "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children:
|
|
113
|
+
return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "helper", "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: "Model", placeholder: "The deployment\u2019s default", models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error?.message ?? null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: "Use the default" })) : null] }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "Instructions", rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Reading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Soft, { children: on.error.message })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization. Switch skills and connectors on first.' })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
123
114
|
await remove(t, agent.id || agent.name);
|
|
124
115
|
onDeleted();
|
|
125
116
|
} })) : null] })) }));
|
package/lib/landing.js
CHANGED
|
@@ -18,13 +18,15 @@ import { Button } from '@hanzo/ui';
|
|
|
18
18
|
import { ModeSelect } from '@hanzo/ui/agents';
|
|
19
19
|
import { Composer } from '@hanzo/ui/chat';
|
|
20
20
|
import { BranchSelect, ChipSelect, FooterLink, HanzoMark, RepoSelect } from '@hanzo/ui/product';
|
|
21
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
22
|
+
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
21
23
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
22
24
|
import { start, unhonoured } from './api/coding.js';
|
|
23
25
|
import { asRepo, chosen, codebases, one } from './api/codebases.js';
|
|
24
26
|
import { read, SETUP } from './api/environment.js';
|
|
25
27
|
import { SetupDialog } from './environment.js';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
+
import { ENSO, limits as readLimits, models } from './api/models.js';
|
|
29
|
+
import { usePick } from './pick.js';
|
|
28
30
|
import { ready, SANDBOX } from './api/places.js';
|
|
29
31
|
import { isForge } from './choice.js';
|
|
30
32
|
import { useKept, usePlaces, useRead } from './data.js';
|
|
@@ -102,6 +104,8 @@ export function Landing({ onStarted }) {
|
|
|
102
104
|
}, []);
|
|
103
105
|
const places = usePlaces(t, signed);
|
|
104
106
|
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
107
|
+
const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
|
|
108
|
+
const [model, pickModel] = usePick(kept.model, (id) => set({ model: id }), catalog.value);
|
|
105
109
|
// The chosen codebase's environment, so New can say when it has none yet.
|
|
106
110
|
const codebase = isForge(kept.repo) ? kept.repo.name : '';
|
|
107
111
|
const env = useRead(signed && codebase ? () => read(t, codebase) : null, null, [t, signed, codebase]);
|
|
@@ -146,7 +150,7 @@ export function Landing({ onStarted }) {
|
|
|
146
150
|
base: repo ? kept.branch : undefined,
|
|
147
151
|
targetId: place.id,
|
|
148
152
|
mode: kept.mode,
|
|
149
|
-
model:
|
|
153
|
+
model: model === ENSO ? undefined : model,
|
|
150
154
|
effort: kept.effort,
|
|
151
155
|
});
|
|
152
156
|
setDraft('');
|
|
@@ -209,7 +213,7 @@ export function Landing({ onStarted }) {
|
|
|
209
213
|
});
|
|
210
214
|
},
|
|
211
215
|
}, placeholder: "New project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
|
|
212
|
-
const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(
|
|
216
|
+
const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ModelPicker, { quiet: true, size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: model, onChange: pickModel, loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
|
|
213
217
|
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", px: "$4", pb: "$10", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, gap: "$5", children: [_jsxs(XStack, { role: "heading", "aria-level": 1, justify: "center", items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(HanzoMark, { size: 26 }), _jsx(SizableText, { size: "$8", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' })] }), _jsxs(YStack, { gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
|
|
214
218
|
? `A proposed environment for ${codebase} is waiting for review.`
|
|
215
219
|
: `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, ...pane, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
|
package/lib/pick.d.ts
ADDED
package/lib/pick.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model a surface starts its next run on, and how a pick changes it.
|
|
3
|
+
*
|
|
4
|
+
* A premium pick holds HERE, for the runs started from this screen, and is
|
|
5
|
+
* never kept as the default — the next New opens on what was kept, Enso unless
|
|
6
|
+
* the person kept another. Any other pick is kept. A kept premium model (from
|
|
7
|
+
* before this rule) is not a default either, so Enso stands in for it.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { ENSO } from './api/models.js';
|
|
11
|
+
export function usePick(kept, keep, catalog) {
|
|
12
|
+
const [once, setOnce] = useState(null);
|
|
13
|
+
const premium = (id) => catalog.find((m) => m.id === id)?.class === 'premium';
|
|
14
|
+
const pick = (id) => {
|
|
15
|
+
if (premium(id))
|
|
16
|
+
return setOnce(id);
|
|
17
|
+
setOnce(null);
|
|
18
|
+
keep(id);
|
|
19
|
+
};
|
|
20
|
+
return [once ?? (premium(kept) ? ENSO : kept), pick];
|
|
21
|
+
}
|
package/lib/run.js
CHANGED
|
@@ -29,10 +29,12 @@ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from
|
|
|
29
29
|
import { Steer } from '@hanzo/ui/agents';
|
|
30
30
|
import { Composer } from '@hanzo/ui/chat';
|
|
31
31
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
32
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
33
|
+
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
32
34
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
33
|
-
import { Access } from './access.js';
|
|
34
35
|
import { approve, followUp, headline, retry, start } from './api/coding.js';
|
|
35
|
-
import { ENSO,
|
|
36
|
+
import { ENSO, limits as readLimits, models } from './api/models.js';
|
|
37
|
+
import { usePick } from './pick.js';
|
|
36
38
|
import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
|
|
37
39
|
import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
|
|
38
40
|
import { verdict } from './api/verdict.js';
|
|
@@ -102,9 +104,10 @@ export function Run({ id }) {
|
|
|
102
104
|
// The model and effort a run started here uses: New's choice, which a change here changes too.
|
|
103
105
|
const { prefs } = usePrefs();
|
|
104
106
|
const [chose, choose] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
|
|
105
|
-
const model = text(chose?.model) || prefs.code?.model || ENSO;
|
|
106
|
-
const pace = EFFORTS.find((e) => e.id === chose?.effort) ?? EFFORTS.find((e) => e.id === prefs.code?.effort) ?? EFFORTS[1];
|
|
107
107
|
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
108
|
+
const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
|
|
109
|
+
const [model, pickModel] = usePick(text(chose?.model) || prefs.code?.model || ENSO, (id) => choose({ ...chose, model: id }), catalog.value);
|
|
110
|
+
const pace = EFFORTS.find((e) => e.id === chose?.effort) ?? EFFORTS.find((e) => e.id === prefs.code?.effort) ?? EFFORTS[1];
|
|
108
111
|
const mode = record?.mode ?? '';
|
|
109
112
|
// A run that narrated no ask of its own opens with the one its title records, once it has said anything.
|
|
110
113
|
const shown = useMemo(() => {
|
|
@@ -323,7 +326,7 @@ export function Run({ id }) {
|
|
|
323
326
|
? 'Follow up — continues in a new run'
|
|
324
327
|
: 'Reading this run…';
|
|
325
328
|
// What a new run from here is made with; words that steer this run use neither.
|
|
326
|
-
const tuning = signed && !steering ? (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", justify: "flex-end", flexWrap: "wrap", rowGap: "$1", children: [_jsx(
|
|
329
|
+
const tuning = signed && !steering ? (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", justify: "flex-end", flexWrap: "wrap", rowGap: "$1", children: [_jsx(ModelPicker, { quiet: true, size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: model, onChange: pickModel, loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: pace.label, chosen: pace, items: EFFORTS, onChange: (e) => choose({ ...chose, effort: e.id }), placement: "top-end", width: 180 })] })) : undefined;
|
|
327
330
|
return (_jsxs(XStack, { ref: measure, "data-slot": "run-split", flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { "data-slot": "run", ...pane, flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
|
|
328
331
|
// A paused run's agent was stopped where it stood; what it was running is cut off.
|
|
329
332
|
live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve')) : undefined, approving: busy, onVerdict: (v) => verdict(t, id, record?.project ?? '', v), empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
|
package/lib/settings/code.js
CHANGED
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
import { YStack } from '@hanzo/gui';
|
|
9
9
|
import { ChevronRight } from '@hanzogui/lucide-icons-2';
|
|
10
10
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
11
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
11
12
|
import { useState } from 'react';
|
|
12
13
|
import { unhonoured } from '../api/coding.js';
|
|
13
|
-
import { Access } from '../access.js';
|
|
14
14
|
import { ENSO, models } from '../api/models.js';
|
|
15
15
|
import { SANDBOX } from '../api/places.js';
|
|
16
16
|
import { usePlaces, useRead } from '../data.js';
|
|
@@ -44,11 +44,12 @@ export function Code() {
|
|
|
44
44
|
}));
|
|
45
45
|
// The sandbox is always listed, so a place not found is a machine that is not: it reads as its id.
|
|
46
46
|
const place = where.find((w) => w.place === now.place) ?? { id: now.place, label: now.place, place: now.place };
|
|
47
|
-
|
|
47
|
+
// A default is never premium: a premium model is picked for one run, in New.
|
|
48
|
+
const offered = catalog.value.filter((m) => m.class !== 'premium');
|
|
48
49
|
const go = (section) => host.go(path({ kind: 'settings', section }));
|
|
49
50
|
const effort = EFFORTS.find((e) => e.id === now.effort);
|
|
50
51
|
const mode = MODES.find((m) => m.id === now.mode);
|
|
51
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Code", detail: "How a new run starts. New begins from these until you choose otherwise there." }), _jsxs(Group, { title: "Defaults", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Model", detail: "Enso picks one for each step; any other model runs them all.", trailing: _jsx(
|
|
52
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Code", detail: "How a new run starts. New begins from these until you choose otherwise there." }), _jsxs(Group, { title: "Defaults", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Model", detail: "Enso picks one for each step; any other model runs them all.", trailing: _jsx(ModelPicker, { size: "sm", name: "Default model", models: offered, scope: "chat", value: now.model || ENSO, onChange: (id) => keep({ model: id }), loading: catalog.loading, error: catalog.error?.message ?? null }) }), _jsx(Row, { title: "Effort", detail: "How long the agent thinks before it acts.", trailing: _jsx(ChipSelect, { name: "Default effort", label: effort.label, chosen: effort, items: EFFORTS, onChange: (e) => keep({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: "Mode", detail: mode.hint, trailing: _jsx(ChipSelect, { name: "Default mode", label: mode.label, chosen: mode, items: MODES, onChange: (m) => keep({ mode: m.id }), placement: "bottom-end", width: 260 }) }), _jsx(Row, { title: "Where it runs", detail: "The Hanzo sandbox, or one of your organization\u2019s machines.", trailing: _jsx(ChipSelect, { name: "Default place", label: place.label, chosen: place, items: where, onChange: (w) => keep({ place: w.place }), placeholder: "Search machines\u2026", placement: "bottom-end", loading: places.loading, error: places.error?.message ?? null }) })] }), _jsx(Note, { children: unhonoured(now.mode, now.place) })] }), _jsx(Group, { title: "Workspace", children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Environments", detail: "What a run installs and starts in each codebase, and the secrets it exports.", trailing: _jsx(Link, { label: "Environments", onPress: () => go('environments') }) }), _jsx(Row, { title: "Machines", detail: "Your organization\u2019s own computers that take runs.", trailing: _jsx(Link, { label: "Machines", onPress: () => go('machines') }) })] }) }), _jsx(Note, { children: note })] }));
|
|
52
53
|
}
|
|
53
54
|
function Link({ label, onPress }) {
|
|
54
55
|
return (_jsx(YStack, { render: "button", "aria-label": `Open ${label}`, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onPress, children: _jsx(ChevronRight, { size: 16 }) }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
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.",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@hanzo/font": "1.9.3",
|
|
12
12
|
"@hanzo/gui": "8.3.5",
|
|
13
|
-
"@hanzo/iam": "0.21.
|
|
14
|
-
"@hanzo/ui": "8.27.
|
|
13
|
+
"@hanzo/iam": "0.21.28",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@hanzo/gui": ">=8.3.5",
|
|
59
|
-
"@hanzo/ui": ">=8.27.
|
|
59
|
+
"@hanzo/ui": ">=8.27.67",
|
|
60
60
|
"@hanzogui/lucide-icons-2": ">=8.3.5",
|
|
61
61
|
"react": ">=19.2.8",
|
|
62
62
|
"@hanzo/voice": ">=0.1.8"
|
package/lib/access.d.ts
DELETED
package/lib/access.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Under a model list: the models it lists and cannot choose, and where to ask
|
|
4
|
-
* for them. Nothing while every model listed answers.
|
|
5
|
-
*
|
|
6
|
-
* A link cannot sit inside a listbox option, so it sits here, under the list,
|
|
7
|
-
* where the keyboard reaches it from the search field.
|
|
8
|
-
*/
|
|
9
|
-
import { SizableText, XStack } from '@hanzo/gui';
|
|
10
|
-
import { Out } from './out.js';
|
|
11
|
-
export function Access({ items }) {
|
|
12
|
-
const locked = items.filter((m) => m.disabled && m.request);
|
|
13
|
-
if (!locked.length)
|
|
14
|
-
return null;
|
|
15
|
-
return (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [locked.map((m) => m.label).join(', '), " ", locked.length > 1 ? 'are' : 'is', " in research preview."] }), _jsx(Out, { href: locked[0].request, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Request access" }) })] }));
|
|
16
|
-
}
|