@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 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
+ }
@@ -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 an agent may think with on a surface a customer reads: Hanzo's own
17
- * SKUs — Enso, Zen and the free tier — by their Hanzo names, and none of the
18
- * embedding, guard, rerank, speech or vision models, which hold no conversation.
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
- export declare function skus(list: readonly Model[]): Model[];
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
- * `enso` is the router: it picks a model per step, which is why it is the
7
- * default and why it is not a model a person has to understand to use.
8
- *
9
- * After what the gateway serves come the models nobody can call yet, from
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 { RESEARCH } from '@hanzo/ui/models/catalog';
11
+ import { parseModels } from '@hanzo/ui/models/catalog';
14
12
  import { call } from './call.js';
15
- export const ENSO = 'enso';
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
- const raw = await call(t, 'GET', '/v1/models');
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
- * The models an agent may think with on a surface a customer reads: Hanzo's own
45
- * SKUs — Enso, Zen and the free tier — by their Hanzo names, and none of the
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
  }
@@ -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 { Access } from './access.js';
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, label as named, models, skus } from './api/models.js';
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 refusal in the platform's words; out of credit names where to add some. */
121
- function said(e) {
122
- if (e instanceof Refusal && e.status === 402)
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}: ${said(e)}`);
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 = fresh ? Boolean(d.name.trim() || d.instructions.trim()) : Object.keys(change).length > 0;
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
- const set = (p) => setD((x) => ({ ...x, ...p }));
213
- const setWhen = (p) => setD((x) => ({ ...x, schedule: { ...x.schedule, ...p } }));
216
+ // An edit answers what the last line asked for, so the line goes with it.
217
+ const set = (p) => {
218
+ setNote('');
219
+ setCredit(false);
220
+ setD((x) => ({ ...x, ...p }));
221
+ };
222
+ const setWhen = (p) => set({ schedule: { ...d.schedule, ...p } });
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
- setNote(said(e));
246
+ // Wanting credit is said in these words only where a run was asked for.
247
+ const run = what === 'run' && broke(e);
248
+ setNote(run ? BROKE : e.message);
249
+ setCredit(run);
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 sent = tidy(d);
251
- const send = changes(base, sent);
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, sent, drafted(now)));
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 back = (how) => host.go(path({ kind: 'screen', screen: 'automations' }), how);
302
+ const leave = (how) => host.go(path({ kind: 'screen', screen: 'automations' }), how);
303
+ // Unsaved work is not left without a word.
304
+ const back = () => (dirty ? setLeaving(true) : leave());
305
+ const begin = (x) => {
306
+ const tz = x.schedule.kind === 'manual' ? undefined : zone();
307
+ set({ name: x.name, instructions: x.instructions, schedule: tz ? { ...x.schedule, tz } : { kind: 'manual' } });
308
+ asksRef.current?.focus();
309
+ };
276
310
  if (!signed)
277
- return _jsx(Page, { title: "Automation", onBack: () => back(), says: "Sign in to see this organization's automations." });
311
+ return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Sign in to see this organization's automations." });
278
312
  if (!fresh && saved.error && !a)
279
- return _jsx(Page, { title: "Automation", onBack: () => back(), says: saved.error.message });
313
+ return _jsx(Page, { title: "Automation", onBack: () => leave(), says: saved.error.message });
280
314
  if (!ready)
281
- return _jsx(Page, { title: "Automation", onBack: () => back(), says: "Reading the automation\u2026" });
315
+ return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Reading the automation\u2026" });
282
316
  const s = d.schedule;
283
- const on = switchesAlone ? Boolean(a?.enabled) : d.enabled;
284
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => back(), "aria-label": "All automations", children: [_jsx(ChevronLeft, { size: 14 }), "Automations"] }), _jsx(XStack, { flex: 1 }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: switchesAlone ? (on ? 'On' : 'Off') : on ? 'On when saved' : 'Off when saved' }), _jsx(Switch, { checked: on, disabled: busy === 'switch', onCheckedChange: flip, "aria-label": switchesAlone ? `${a.name} on` : 'On when saved' })] }), a && !a.draft ? (_jsx(Button, { size: "sm", variant: "outline", disabled: Boolean(busy) || dirty, onPress: () => void go(), children: busy === 'run' ? 'Starting…' : 'Run now' })) : null] }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", numberOfLines: 1, children: fresh ? 'New automation' : a?.name }), _jsx(SizableText, { size: "$2", color: "$soft", children: fresh
285
- ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
286
- : a?.draft
287
- ? 'A draft: say what it should do and save it, and it runs.'
288
- : [words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), _jsx(Field, { label: "Name", children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh }) }), _jsx(Field, { label: "Instructions", hint: "What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Summarize what needs my attention today across calendar, email and messages.", "aria-label": "Instructions", rows: 7 }) }), _jsx(Field, { label: "When it runs", hint: s.kind === 'cron' ? 'Five fields: minute, hour, day of month, month, day of week.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: shaped(k.id, s) }), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: "Day", label: DAYS.find((x) => x.id === s.day)?.label ?? 'Monday', chosen: DAYS.find((x) => x.id === s.day), items: DAYS, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": "Time of day", inputMode: "text" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "at minute" }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": "Minute", inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": "Cron", autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: "Time zone", label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: "Search zones\u2026", placement: "bottom-start", width: 260 }) : null] }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ChipSelect, { name: "Model", label: chosen.label, chosen: chosen, items: items, footer: _jsx(Access, { items: items }), onChange: (m) => set({ model: m.id === ENSO ? null : m.id }), placeholder: "Search models\u2026", placement: "bottom-start", loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Permissions", hint: d.permissions === 'auto' ? 'Works and uses connectors without stopping.' : 'Reads and researches, changes nothing, and ends with the actions it would take for you to approve.', children: _jsx(Choice, { label: "Permissions", value: d.permissions, options: PERMISSIONS, onChange: (p) => set({ permissions: p }) }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Email me when a run ends" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "One line on how it went, with a link to the run." })] }), _jsx(Switch, { checked: d.notify, onCheckedChange: (v) => set({ notify: v }), "aria-label": "Email me when a run ends" })] }), _jsx(Line, { children: note || (dirty ? wrong : '') }), _jsxs(XStack, { gap: "$2", items: "center", children: [a ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), a && dirty ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => {
317
+ // A new automation or a draft has nothing to switch on until it says what to do.
318
+ const can = switchesAlone || Boolean(d.instructions.trim());
319
+ const on = switchesAlone ? Boolean(a?.enabled) : can && d.enabled;
320
+ const says = switchesAlone ? (on ? 'On' : 'Off') : !can ? 'Add instructions first' : on ? 'On when saved' : 'Off when saved';
321
+ const state = fresh ? 'Not created yet' : dirty ? 'Unsaved changes' : '';
322
+ const sayId = `${id}-switch`;
323
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { shrink: 0, px: "$6", pt: "$4", pb: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: back, "aria-label": "All automations", children: [_jsx(ChevronLeft, { size: 14 }), "Automations"] }), _jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: state }), _jsx(XStack, { flex: 1 }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: sayId, size: "$2", color: "$soft", children: says }), _jsx(Switch, { checked: on, disabled: busy === 'switch' || !can, onCheckedChange: flip, "aria-label": switchesAlone ? `${a.name} on` : 'On when saved', "aria-describedby": sayId })] }), a && !a.draft ? (_jsx(Button, { size: "sm", variant: "outline", disabled: Boolean(busy) || dirty, onPress: () => void go(), children: busy === 'run' ? 'Starting…' : 'Run now' })) : null, a && dirty ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => {
289
324
  setBase(drafted(a));
290
325
  setD(drafted(a));
291
- }, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || !dirty || !d.name.trim() || !d.instructions.trim() || Boolean(wrong), onPress: () => void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), a ? (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Runs" }), history.error && !history.value.length ? (_jsx(Line, { children: history.error.message })) : !history.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: history.loading ? 'Reading its runs…' : a.draft ? 'It runs once its instructions are saved.' : 'No runs yet. Run now starts one.' })) : (_jsx(YStack, { role: "list", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", "aria-label": "Run history", children: history.value.map((r, i) => (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(YStack, { pt: "$1", children: _jsx(StatusDot, { status: DOT[r.status] ?? 'idle' }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `${WORD[r.status] ?? r.status} · ${local(r.at)}` }), _jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: r.summary || (live(r.status) ? 'Working…' : '') })] }), r.session ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go(r.session), "aria-label": `Open the Dev run of ${local(r.at)}`, children: "Open run" })) : null] }, r.id))) }))] })) : null] }), a ? (_jsx(Confirm, { what: a.name, says: "It stops running, and its schedule and its runs go with it. A run going now is stopped.", open: asking, onOpenChange: setAsking, onYes: async () => {
292
- await remove(t, a.id);
293
- back({ replace: true });
294
- } })) : null] }));
326
+ setNote('');
327
+ }, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || !(fresh || drafty || dirty), onPress: () => complete() && void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), note ? (_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", pt: "$2", children: [_jsx(Line, { children: note }), credit ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Add credit" })) : null] })) : null] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$5", children: [_jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$5", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", numberOfLines: 1, children: fresh ? 'New automation' : a?.name }), _jsx(SizableText, { size: "$2", color: "$soft", children: fresh
328
+ ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
329
+ : a?.draft
330
+ ? 'A draft: say what it should do and save it, and it runs.'
331
+ : [words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), fresh && offered.value.length && !d.instructions.trim() ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Start from" }), offered.value.map((x) => (_jsx(Button, { size: "sm", variant: "outline", onPress: () => begin(x), "aria-label": `Start from ${x.name}`, children: x.name }, x.key)))] })) : null, _jsx(Field, { label: "Name", children: _jsx(Input, { ref: nameRef, value: d.name, onChangeText: (v) => set({ name: v }), onSubmitEditing: () => asksRef.current?.focus(), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh }) }), _jsx(Field, { label: "Instructions (required)", hint: "What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills.", children: _jsx(Textarea, { ref: asksRef, value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "What should it do each run? For example: summarize what needs my attention today across calendar, email and messages.", "aria-label": "Instructions", rows: 7 }) }), _jsx(Field, { label: "When it runs", hint: s.kind === 'cron' ? 'Five fields: minute, hour, day of month, month, day of week.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: shaped(k.id, s) }), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: "Day", label: DAYS.find((x) => x.id === s.day)?.label ?? 'Monday', chosen: DAYS.find((x) => x.id === s.day), items: DAYS, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": "Time of day", inputMode: "text" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "at minute" }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": "Minute", inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": "Cron", autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: "Time zone", label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: "Search zones\u2026", placement: "bottom-start", width: 260 }) : null] }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: d.model ?? ENSO, onChange: (id) => set({ model: id === ENSO ? null : id }), loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Permissions", hint: d.permissions === 'auto' ? 'Works and uses connectors without stopping.' : 'Reads and researches, changes nothing, and ends with the actions it would take for you to approve.', children: _jsx(Choice, { label: "Permissions", value: d.permissions, options: PERMISSIONS, onChange: (p) => set({ permissions: p }) }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Email me when a run ends" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "One line on how it went, with a link to the run." })] }), _jsx(Switch, { checked: d.notify, onCheckedChange: (v) => set({ notify: v }), "aria-label": "Email me when a run ends" })] }), dirty && wrong && note !== wrong ? _jsx(Line, { children: wrong }) : null, a ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: "Delete" }) })) : null, a ? (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Runs" }), history.error && !history.value.length ? (_jsx(Line, { children: history.error.message })) : !history.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: history.loading ? 'Reading its runs…' : a.draft ? 'It runs once its instructions are saved.' : 'No runs yet. Run now starts one.' })) : (_jsx(YStack, { role: "list", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", "aria-label": "Run history", children: history.value.map((r, i) => (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(YStack, { pt: "$1", children: _jsx(StatusDot, { status: DOT[r.status] ?? 'idle' }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `${WORD[r.status] ?? r.status} · ${local(r.at)}` }), _jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: r.summary || (live(r.status) ? 'Working…' : '') })] }), r.session ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go(r.session), "aria-label": `Open the Dev run of ${local(r.at)}`, children: "Open run" })) : null] }, r.id))) }))] })) : null] }), a ? (_jsx(Confirm, { what: a.name, says: "It stops running, and its schedule and its runs go with it. A run going now is stopped.", open: asking, onOpenChange: setAsking, onYes: async () => {
332
+ await remove(t, a.id);
333
+ leave({ replace: true });
334
+ } })) : null, _jsxs(Sheet, { title: "Leave without saving?", open: leaving, onOpenChange: setLeaving, width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: fresh ? 'This automation is not created yet.' : 'Your changes to this automation are not saved.' }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: "Keep editing" }), _jsx(Button, { size: "sm", variant: "destructive", onPress: () => leave(), children: "Leave" })] })] })] })] }));
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 })] }) }));
@@ -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 { ChipSelect } from '@hanzo/ui/product';
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 { Access } from '../access.js';
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: _jsx(XStack, { children: _jsx(ChipSelect, { name: "Model", label: chosen?.label ?? 'Default', chosen: chosen, items: items, footer: _jsx(Access, { items: items }), onChange: (m) => set({ model: m.id === DEFAULT ? '' : m.id }), placeholder: "Search models\u2026", placement: "bottom-start", loading: catalog.loading, error: catalog.error?.message ?? 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 () => {
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 { Access } from './access.js';
27
- import { ENSO, models } from './api/models.js';
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: kept.model === ENSO ? undefined : kept.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(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", 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 })] }));
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
@@ -0,0 +1,2 @@
1
+ import { type Model } from './api/models.ts';
2
+ export declare function usePick(kept: string, keep: (id: string) => void, catalog: readonly Model[]): readonly [string, (id: string) => void];
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, label as named, models } from './api/models.js';
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(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === model)?.label ?? named(model), chosen: catalog.value.find((m) => m.id === model) ?? null, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => choose({ ...chose, model: m.id }), placeholder: "Search models\u2026", placement: "top-end", 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;
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
@@ -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
- const model = catalog.value.find((m) => m.id === now.model) ?? { id: now.model, label: now.model === ENSO ? 'Enso' : now.model };
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(ChipSelect, { name: "Default model", label: model.label, chosen: model, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => keep({ model: m.id }), placeholder: "Search models\u2026", placement: "bottom-end", 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
+ 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.41",
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.21",
14
- "@hanzo/ui": "8.27.22",
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.22",
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
@@ -1,4 +0,0 @@
1
- import type { Model } from './api/models.ts';
2
- export declare function Access({ items }: {
3
- items: readonly Model[];
4
- }): import("react").JSX.Element | null;
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
- }