@hanzo/build 0.2.41 → 0.2.42

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.
@@ -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,13 +15,14 @@ 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
23
  import { automation, automations, changes, create, remove, runs, save, start, 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 { ENSO, limits as readLimits, models } from './api/models.js';
25
26
  import { Choice, Confirm, Field, Line } from './customize/ui.js';
26
27
  import { useRead } from './data.js';
27
28
  import { useHost, useTarget } from './host.js';
@@ -166,6 +167,7 @@ export function Editor({ id }) {
166
167
  const saved = useRead(signed && !fresh ? () => automation(t, id) : null, null, [t, signed, id, tick]);
167
168
  const history = useRead(signed && !fresh ? () => runs(t, id) : null, [], [t, signed, id, beat]);
168
169
  const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
170
+ const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
169
171
  // `base` is what the form started from; a save sends what changed since, and only that.
170
172
  const [base, setBase] = useState(BLANK);
171
173
  const [d, setD] = useState(BLANK);
@@ -212,12 +214,6 @@ export function Editor({ id }) {
212
214
  const set = (p) => setD((x) => ({ ...x, ...p }));
213
215
  const setWhen = (p) => setD((x) => ({ ...x, schedule: { ...x.schedule, ...p } }));
214
216
  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
217
  const zones = useMemo(() => {
222
218
  try {
223
219
  return ['UTC', ...Intl.supportedValuesOf('timeZone').filter((z) => z !== 'UTC')].map((z) => ({ id: z, label: z }));
@@ -285,7 +281,7 @@ export function Editor({ id }) {
285
281
  ? 'Name it, say what it should do, and when. It runs as you, in this organization.'
286
282
  : a?.draft
287
283
  ? '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: () => {
284
+ : [words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), _jsx(Field, { label: "Name", children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh }) }), _jsx(Field, { label: "Instructions", hint: "What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Summarize what needs my attention today across calendar, email and messages.", "aria-label": "Instructions", rows: 7 }) }), _jsx(Field, { label: "When it runs", hint: s.kind === 'cron' ? 'Five fields: minute, hour, day of month, month, day of week.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: shaped(k.id, s) }), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: "Day", label: DAYS.find((x) => x.id === s.day)?.label ?? 'Monday', chosen: DAYS.find((x) => x.id === s.day), items: DAYS, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": "Time of day", inputMode: "text" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "at minute" }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": "Minute", inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": "Cron", autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: "Time zone", label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: "Search zones\u2026", placement: "bottom-start", width: 260 }) : null] }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: d.model ?? ENSO, onChange: (id) => set({ model: id === ENSO ? null : id }), loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Permissions", hint: d.permissions === 'auto' ? 'Works and uses connectors without stopping.' : 'Reads and researches, changes nothing, and ends with the actions it would take for you to approve.', children: _jsx(Choice, { label: "Permissions", value: d.permissions, options: PERMISSIONS, onChange: (p) => set({ permissions: p }) }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Email me when a run ends" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "One line on how it went, with a link to the run." })] }), _jsx(Switch, { checked: d.notify, onCheckedChange: (v) => set({ notify: v }), "aria-label": "Email me when a run ends" })] }), _jsx(Line, { children: note || (dirty ? wrong : '') }), _jsxs(XStack, { gap: "$2", items: "center", children: [a ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), a && dirty ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => {
289
285
  setBase(drafted(a));
290
286
  setD(drafted(a));
291
287
  }, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || !dirty || !d.name.trim() || !d.instructions.trim() || Boolean(wrong), onPress: () => void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), a ? (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Runs" }), history.error && !history.value.length ? (_jsx(Line, { children: history.error.message })) : !history.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: history.loading ? 'Reading its runs…' : a.draft ? 'It runs once its instructions are saved.' : 'No runs yet. Run now starts one.' })) : (_jsx(YStack, { role: "list", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", "aria-label": "Run history", children: history.value.map((r, i) => (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(YStack, { pt: "$1", children: _jsx(StatusDot, { status: DOT[r.status] ?? 'idle' }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `${WORD[r.status] ?? r.status} · ${local(r.at)}` }), _jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: r.summary || (live(r.status) ? 'Working…' : '') })] }), r.session ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go(r.session), "aria-label": `Open the Dev run of ${local(r.at)}`, children: "Open run" })) : null] }, r.id))) }))] })) : null] }), a ? (_jsx(Confirm, { what: a.name, says: "It stops running, and its schedule and its runs go with it. A run going now is stopped.", open: asking, onOpenChange: setAsking, onYes: async () => {
@@ -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.42",
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.67",
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
- }