@hanzo/build 0.2.61 → 0.2.63

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/gear.d.ts CHANGED
@@ -49,3 +49,5 @@ export interface GearState {
49
49
  * writes the default until it has an id). Signed out, it lives for the page.
50
50
  */
51
51
  export declare function useGear(t: Target, key?: string): GearState;
52
+ /** The dictation language the person keeps, null for Auto, and how to change it. */
53
+ export declare function useLanguage(t: Target): readonly [string | null, (id: string | null) => void];
package/lib/gear.js CHANGED
@@ -62,7 +62,7 @@ export function shelve(gears, key, g) {
62
62
  }
63
63
  return out;
64
64
  }
65
- let doc = { gear: BARE, gears: {} };
65
+ let doc = { gear: BARE, gears: {}, language: null };
66
66
  let loaded = null;
67
67
  const listeners = new Set();
68
68
  const emit = () => listeners.forEach((f) => f());
@@ -80,7 +80,8 @@ function load(t) {
80
80
  const raw = r?.prefs ?? {};
81
81
  const gear = gearOf(raw.gear);
82
82
  const all = raw.gears && typeof raw.gears === 'object' ? raw.gears : {};
83
- doc = { gear, gears: Object.fromEntries(Object.entries(all).map(([k, v]) => [k, gearOf(v, gear)])) };
83
+ const language = typeof raw.language === 'string' && raw.language ? raw.language : null;
84
+ doc = { gear, gears: Object.fromEntries(Object.entries(all).map(([k, v]) => [k, gearOf(v, gear)])), language };
84
85
  emit();
85
86
  })
86
87
  .catch(() => {
@@ -121,3 +122,20 @@ export function useGear(t, key = '') {
121
122
  },
122
123
  }), [gear, error, write, t]);
123
124
  }
125
+ /** The dictation language the person keeps, null for Auto, and how to change it. */
126
+ export function useLanguage(t) {
127
+ useEffect(() => load(t), [t]);
128
+ const now = useSyncExternalStore(subscribe, () => doc, () => doc);
129
+ const set = useCallback((id) => {
130
+ const was = doc.language;
131
+ doc = { ...doc, language: id };
132
+ emit();
133
+ // A refused save takes the choice back, so what is shown is what is kept.
134
+ if (t.token())
135
+ call(t, 'PATCH', '/v1/pref', { language: id }).catch(() => {
136
+ doc = { ...doc, language: was };
137
+ emit();
138
+ });
139
+ }, [t]);
140
+ return [now.language, set];
141
+ }
package/lib/index.d.ts CHANGED
@@ -51,7 +51,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Scree
51
51
  export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
52
52
  export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload, type Chip, type PromptOptions } from './prompt.tsx';
53
53
  export { Plus, type Link, type PlusProps, type Row as ShelfRow, type Shelf, type Switch } from './plus.tsx';
54
- export { BARE, gearOf, MODES, useGear, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
54
+ export { BARE, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
55
55
  export { useCarry, type Carry } from './carry.tsx';
56
56
  export { Dictation } from './mic.tsx';
57
57
  export type { Composer as PluginComposer, Plugin, Prep, Turn } from './plugin.ts';
package/lib/index.js CHANGED
@@ -46,7 +46,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
46
46
  export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
47
47
  export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload } from './prompt.js';
48
48
  export { Plus } from './plus.js';
49
- export { BARE, gearOf, MODES, useGear } from './gear.js';
49
+ export { BARE, gearOf, MODES, useGear, useLanguage } from './gear.js';
50
50
  export { useCarry } from './carry.js';
51
51
  export { Dictation } from './mic.js';
52
52
  export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker } from './reply.js';
package/lib/mic.d.ts CHANGED
@@ -2,9 +2,11 @@ import { type Machine } from '@hanzo/voice';
2
2
  export interface DictationProps {
3
3
  voice: Machine;
4
4
  disabled?: boolean;
5
+ /** A phone: both halves take a finger's width. */
6
+ narrow?: boolean;
5
7
  /** The language dictation listens in, or null for Auto (the browser's own). */
6
8
  language?: string | null;
7
9
  onLanguage?: (id: string | null) => void;
8
10
  }
9
11
  /** The mic and its chevron, one control on the composer's row. */
10
- export declare function Dictation({ voice, disabled, language, onLanguage }: DictationProps): import("react").JSX.Element;
12
+ export declare function Dictation({ voice, disabled, narrow, language, onLanguage }: DictationProps): import("react").JSX.Element;
package/lib/mic.js CHANGED
@@ -4,18 +4,43 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  * microphones this browser has, the chosen one's live level, and the language
5
5
  * dictation listens in (Auto is the browser's own).
6
6
  *
7
- * The microphones are listed as the browser names them, the system's own
8
- * input first; dictation opens that one. The meter listens only while the menu
9
- * is open, and only where the person has already let the page use a
10
- * microphone: opening a menu never asks for permission.
7
+ * The chosen microphone is this browser's (a device id means nothing on
8
+ * another), kept in localStorage and handed to @hanzo/voice (`choose`), so
9
+ * every capture opens it; unchosen, the system's own input. The meter listens
10
+ * only while the menu is open, and only where the person has already let the
11
+ * page use a microphone: opening a menu never asks for permission.
11
12
  */
12
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
13
14
  import { Check, ChevronDown, Mic as MicIcon } from '@hanzogui/lucide-icons-2';
14
15
  import { Control } from '@hanzo/composer';
15
16
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@hanzo/ui';
16
- import { Voice } from '@hanzo/voice';
17
+ import { choose, Voice } from '@hanzo/voice';
17
18
  import { useEffect, useRef, useState } from 'react';
18
19
  import { LANGUAGES } from './voice.js';
20
+ const KEY = 'hanzo.mic';
21
+ function kept() {
22
+ try {
23
+ return window.localStorage.getItem(KEY);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function keep(id) {
30
+ try {
31
+ if (id)
32
+ window.localStorage.setItem(KEY, id);
33
+ else
34
+ window.localStorage.removeItem(KEY);
35
+ }
36
+ catch {
37
+ // Kept for the page: `choose` holds it.
38
+ }
39
+ choose(id);
40
+ }
41
+ // The page's choice reaches every capture from the first one.
42
+ if (typeof window !== 'undefined')
43
+ choose(kept());
19
44
  /** The mic is a plain mark until it is pointed at. */
20
45
  const PLAIN = { bg: 'transparent', color: 'var(--foreground)', hoverStyle: { bg: '$hover' } };
21
46
  /** The microphones this browser lists; labels only once the page may use one. */
@@ -42,8 +67,8 @@ function useInputs(open) {
42
67
  }, [open]);
43
68
  return { inputs, allowed, using };
44
69
  }
45
- /** The microphone's level while the menu is open, drawn without re-rendering. */
46
- function Meter({ on }) {
70
+ /** The chosen microphone's level while the menu is open, drawn without re-rendering. */
71
+ function Meter({ device, on }) {
47
72
  const bar = useRef(null);
48
73
  useEffect(() => {
49
74
  if (!on)
@@ -53,7 +78,7 @@ function Meter({ on }) {
53
78
  let frame = 0;
54
79
  let live = true;
55
80
  void navigator.mediaDevices
56
- .getUserMedia({ audio: true })
81
+ .getUserMedia({ audio: device ? { deviceId: { exact: device } } : true })
57
82
  .then((s) => {
58
83
  if (!live)
59
84
  return s.getTracks().forEach((t) => t.stop());
@@ -81,15 +106,22 @@ function Meter({ on }) {
81
106
  stream?.getTracks().forEach((t) => t.stop());
82
107
  void ctx?.close();
83
108
  };
84
- }, [on]);
109
+ }, [device, on]);
85
110
  return (_jsxs(XStack, { items: "center", gap: 10, px: 8, py: 8, "data-slot": "mic-meter", "aria-hidden": true, children: [_jsx(MicIcon, { size: 14, color: "$hint" }), _jsx(YStack, { flex: 1, height: 4, rounded: 2, bg: "$hover", overflow: "hidden", children: _jsx(YStack, { ref: bar, height: 4, width: "100%", bg: "$ink",
86
111
  // inline-style: the level is written per frame onto the node; a style prop would mint a class per value.
87
112
  style: { transform: 'scaleX(0)', transformOrigin: 'left' } }) })] }));
88
113
  }
89
114
  /** The mic and its chevron, one control on the composer's row. */
90
- export function Dictation({ voice, disabled = false, language = null, onLanguage }) {
115
+ export function Dictation({ voice, disabled = false, narrow = false, language = null, onLanguage }) {
91
116
  const [open, setOpen] = useState(false);
92
117
  const { inputs, allowed, using } = useInputs(open);
118
+ const [device, setDevice] = useState(() => (typeof window === 'undefined' ? null : kept()));
93
119
  const spoken = LANGUAGES.find((l) => l.id === language);
94
- return (_jsxs(XStack, { "data-slot": "dictation", items: "center", rounded: 8, borderWidth: 1, borderColor: open ? '$borderColor' : 'transparent', hoverStyle: { borderColor: '$borderColor' }, shrink: 0, children: [_jsx(Control, { asChild: true, size: 30, ...PLAIN, children: _jsx(Voice, { voice: voice, disabled: disabled, says: { idle: 'Dictate', listening: 'Dictating — click to stop' }, children: (state) => _jsx(MicIcon, { size: 16, fill: state === 'idle' ? 'none' : 'currentColor' }) }) }), _jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, disabled: disabled, children: _jsx(XStack, { render: "button", "aria-label": "Dictation settings", "data-slot": "dictation-menu", width: 16, height: 30, items: "center", justify: "center", rounded: 6, cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, "$max-sm": { width: 28, height: 44 }, children: _jsx(ChevronDown, { size: 12, color: "$hint" }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 200, "data-slot": "dictation-panel", children: [_jsx(Meter, { on: open && allowed }), inputs.length === 0 ? (_jsx(DropdownMenuLabel, { children: _jsx(SizableText, { fontSize: 14, color: "$hint", children: "No devices found" }) })) : (inputs.map((d, i) => (_jsx(DropdownMenuLabel, { children: _jsxs(XStack, { items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", numberOfLines: 1, flex: 1, children: d.label || `Microphone ${i + 1}` }), d.groupId === using ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-label": "In use" }) : null] }) }, d.deviceId || i)))), onLanguage ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: _jsx(SizableText, { fontSize: 13, color: "$hint", children: "Dictation settings" }) }), _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { "data-slot": "dictation-language", children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", flex: 1, children: "Language" }), _jsx(SizableText, { fontSize: 14, color: "$hint", children: spoken?.label ?? 'Auto' })] }) }), _jsx(DropdownMenuSubContent, { minW: 160, children: [{ id: '', label: 'Auto' }, ...LANGUAGES].map((l) => (_jsx(DropdownMenuItem, { onSelect: () => onLanguage(l.id || null), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", flex: 1, children: l.label }), (language ?? '') === l.id ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, l.id || 'auto'))) })] })] })) : null] })] })] }));
120
+ return (_jsxs(XStack, { "data-slot": "dictation", items: "center", rounded: 8, borderWidth: 1, borderColor: open ? '$borderColor' : 'transparent', hoverStyle: { borderColor: '$borderColor' }, shrink: 0, children: [_jsx(Control, { asChild: true, size: narrow ? 44 : 30, ...PLAIN, children: _jsx(Voice, { voice: voice, disabled: disabled, says: { idle: 'Dictate', listening: 'Dictating — click to stop' }, children: (state) => _jsx(MicIcon, { size: 16, color: "var(--foreground)", fill: state === 'idle' ? 'none' : 'var(--foreground)' }) }) }), _jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, disabled: disabled, children: _jsx(XStack, { render: "button", "aria-label": "Dictation settings", "data-slot": "dictation-menu", width: 16, height: 30, items: "center", justify: "center", rounded: 6, cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, ...(narrow ? { width: 44, height: 44 } : null), children: _jsx(ChevronDown, { size: 14, color: "$aside" }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 200, "data-slot": "dictation-panel", children: [_jsx(Meter, { device: device, on: open && allowed }), inputs.length === 0 ? (_jsx(DropdownMenuLabel, { children: _jsx(SizableText, { fontSize: 14, color: "$hint", children: "No devices found" }) })) : (inputs.map((d, i) => {
121
+ const on = device ? d.deviceId === device : d.groupId === using;
122
+ return (_jsx(DropdownMenuItem, { preventCloseOnSelect: true, onSelect: () => {
123
+ setDevice(d.deviceId);
124
+ keep(d.deviceId);
125
+ }, children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", numberOfLines: 1, flex: 1, children: d.label || `Microphone ${i + 1}` }), on ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, d.deviceId || i));
126
+ })), onLanguage ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: _jsx(SizableText, { fontSize: 13, color: "$hint", children: "Dictation settings" }) }), _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { "data-slot": "dictation-language", children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", flex: 1, children: "Language" }), _jsx(SizableText, { fontSize: 14, color: "$hint", children: spoken?.label ?? 'Auto' })] }) }), _jsx(DropdownMenuSubContent, { minW: 160, children: [{ id: '', label: 'Auto' }, ...LANGUAGES].map((l) => (_jsx(DropdownMenuItem, { onSelect: () => onLanguage(l.id || null), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", flex: 1, children: l.label }), (language ?? '') === l.id ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, l.id || 'auto'))) })] })] })) : null] })] })] }));
95
127
  }
package/lib/prompt.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare const measure: {
16
16
  readonly maxW: "calc(var(--container-prose, 48rem) - 1.5rem)";
17
17
  readonly mx: 'auto';
18
18
  };
19
- /** The round controls' box, px. */
19
+ /** The round controls' box, px, and a phone's, a finger's width. */
20
20
  export declare const ROUND = 32;
21
21
  /** What every Composer placeholder says, unless a surface names who is listening. */
22
22
  export declare const ASK = "Type / for skills";
package/lib/prompt.js CHANGED
@@ -37,13 +37,18 @@ export const MEASURE = 'var(--container-prose, 48rem)';
37
37
  export const GAP = 20;
38
38
  /** The composer's width: the measure less the thread column's 0.75rem gutters, centred, so its edges are the text's. */
39
39
  export const measure = { width: 'calc(100% - 1.5rem)', maxW: `calc(${MEASURE} - 1.5rem)`, mx: 'auto' };
40
- /** The round controls' box, px. */
40
+ /** The round controls' box, px, and a phone's, a finger's width. */
41
41
  export const ROUND = 32;
42
+ const TAP = 44;
42
43
  /** The frame's corner, px: 12 on a laptop, 22 on a phone. */
43
44
  const CORNER = 12;
44
45
  const CORNER_PHONE = 22;
46
+ /** The ink on a filled round: the accent's own foreground (gui's icons default to the theme's text). */
47
+ const ON = 'var(--primary-foreground)';
45
48
  /** A round control with no ground of its own until it is pointed at. */
46
49
  const QUIET = { bg: 'transparent', color: 'var(--foreground)', hoverStyle: { bg: '$hover' } };
50
+ /** The model rows the menu draws before anything is typed. */
51
+ const SHOWN = 40;
47
52
  /** What every Composer placeholder says, unless a surface names who is listening. */
48
53
  export const ASK = 'Type / for skills';
49
54
  /**
@@ -79,15 +84,27 @@ function Opens({ label, word, dim, children }) {
79
84
  function Model({ mind, onUpgrade }) {
80
85
  const [open, setOpen] = useState(false);
81
86
  const [q, setQ] = useState('');
82
- const groups = useMemo(() => {
87
+ // The catalog is hundreds long, and drawing all of it is what made the menu slow
88
+ // to open: until something is typed, the first `SHOWN` rows, Hanzo's families
89
+ // first, and a line saying the search reaches the rest.
90
+ const { groups, more } = useMemo(() => {
83
91
  const list = mind.models.filter((m) => !ROUTER_IDS.has(m.id) && (m.outputs?.includes('text') ?? true) && (!q.trim() || matchesModel(m, q)));
84
- return groupModels(list);
92
+ const all = groupModels(list);
93
+ let room = q.trim() ? Infinity : SHOWN;
94
+ const cut = all
95
+ .map((g) => {
96
+ const models = g.models.slice(0, Math.max(0, room));
97
+ room -= models.length;
98
+ return { ...g, models };
99
+ })
100
+ .filter((g) => g.models.length);
101
+ return { groups: cut, more: list.length - cut.reduce((n, g) => n + g.models.length, 0) };
85
102
  }, [mind.models, q]);
86
103
  const routed = mind.model === ENSO;
87
- return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, placement: "top-end", children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { "data-slot": "mind", children: _jsx(Word, { label: `Model: ${nameOf(mind.model, mind.models)}`, open: open, children: nameOf(mind.model, mind.models) }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 280, maxH: 420, "data-slot": "model-menu", children: [_jsx(DropdownMenuSearch, { value: q, onValueChange: setQ, placeholder: "Search models" }), !mind.free && !q.trim() ? (_jsx(DropdownMenuItem, { onSelect: () => mind.pick(ENSO), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsxs(YStack, { flex: 1, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", children: "Enso" }), _jsx(SizableText, { fontSize: 12, color: "$hint", children: "Picks the model for each message and step" })] }), routed ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) })) : null, groups.map((g) => (_jsxs(YStack, { role: "group", "aria-label": g.label, children: [_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, pt: 8, pb: 2, children: g.label }), g.models.map((m) => {
88
- const ok = grants(mind.free, m.id, mind.models);
89
- return (_jsx(DropdownMenuItem, { "data-slot": "model-row", onSelect: () => (ok ? mind.pick(m.id) : onUpgrade?.(m.id)), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: ok ? '$ink' : '$hint', numberOfLines: 1, flex: 1, children: nameOf(m.id, mind.models) }), ok ? null : (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 6, py: 1, rounded: 6, borderWidth: 1, borderColor: "$borderColor", children: "Upgrade" })), m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
90
- })] }, g.key)))] })] }));
104
+ return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, placement: "top-end", children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { "data-slot": "mind", children: _jsx(Word, { label: `Model: ${nameOf(mind.model, mind.models)}`, open: open, children: nameOf(mind.model, mind.models) }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 280, "data-slot": "model-menu", children: [_jsx(DropdownMenuSearch, { value: q, onValueChange: setQ, placeholder: "Search models" }), _jsxs(YStack, { maxH: "min(420px, 60vh)", overflowY: "auto", overflowX: "hidden", "data-slot": "model-rows", children: [!mind.free && !q.trim() ? (_jsx(DropdownMenuItem, { onSelect: () => mind.pick(ENSO), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsxs(YStack, { flex: 1, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", children: "Enso" }), _jsx(SizableText, { fontSize: 12, color: "$hint", children: "Picks the model for each message and step" })] }), routed ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) })) : null, groups.map((g) => (_jsxs(YStack, { role: "group", "aria-label": g.label, children: [_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, pt: 8, pb: 2, children: g.label }), g.models.map((m) => {
105
+ const ok = grants(mind.free, m.id, mind.models);
106
+ return (_jsx(DropdownMenuItem, { "data-slot": "model-row", onSelect: () => (ok ? mind.pick(m.id) : onUpgrade?.(m.id)), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: ok ? '$ink' : '$hint', numberOfLines: 1, flex: 1, children: nameOf(m.id, mind.models) }), ok ? null : (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 6, py: 1, rounded: 6, borderWidth: 1, borderColor: "$borderColor", children: "Upgrade" })), m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
107
+ })] }, g.key))), more > 0 ? (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, py: 8, children: `Type to search ${more} more models` })) : null] })] })] }));
91
108
  }
92
109
  /** The row under the frame: where (Dev), then the model, the effort and the mode. */
93
110
  export function Tune({ mind, mode, where, onUpgrade }) {
@@ -127,15 +144,15 @@ export function prompt(o, state) {
127
144
  };
128
145
  const mention = listed && shelf && typed ? (_jsx(Mention, { shelf: shelf, rows: listed, index: index, onPick: choose, tag: typed.char === '/' ? 'Skill' : 'Plugin', name: typed.char === '/' ? 'Skills' : 'Plugins' })) : null;
129
146
  const head = o.head || o.note || mention || (narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: "$1.5", "data-slot": "prompt-head", children: [narrow ? o.below : null, o.head, mention, o.note ? (_jsx(SizableText, { "data-slot": "prompt-note", fontSize: 12, color: "$soft", text: "center", self: "center", children: o.note })) : null] })) : undefined;
130
- const foot = o.mind || o.mode || o.where || (!narrow && o.below) ? (_jsxs(YStack, { width: "100%", gap: 10, "data-slot": "prompt-foot", children: [_jsx(Tune, { mind: o.mind, mode: o.mode, where: o.where, onUpgrade: o.onUpgrade }), narrow ? null : o.below] })) : undefined;
131
- const round = (_jsx(Control, { type: "button", size: ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": busy ? 'Stop' : 'Send', "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
147
+ const foot = o.mind || o.mode || o.where || (!narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: 10, "data-slot": "prompt-foot", children: [_jsx(Tune, { mind: o.mind, mode: o.mode, where: o.where, onUpgrade: o.onUpgrade }), narrow ? null : o.below] })) : undefined;
148
+ const round = (_jsx(Control, { type: "button", size: narrow ? TAP : ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": busy ? 'Stop' : 'Send', "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
132
149
  if (!live)
133
150
  return;
134
151
  if (busy)
135
152
  o.onStop?.();
136
153
  else
137
154
  o.onSend();
138
- }, children: busy ? _jsx(Square, { size: 13, fill: "currentColor" }) : _jsx(ArrowUp, { size: 16, strokeWidth: 2.5 }) }));
155
+ }, children: busy ? _jsx(Square, { size: 13, color: ON, fill: ON }) : _jsx(ArrowUp, { size: 16, strokeWidth: 2.5, color: ON }) }));
139
156
  return {
140
157
  value: o.value,
141
158
  onChange: o.onChange,
@@ -171,7 +188,7 @@ export function prompt(o, state) {
171
188
  '$max-sm': { rounded: CORNER_PHONE },
172
189
  ...(o.framed ? { boxShadow: 'var(--shadow-sheet-1)' } : null),
173
190
  children: (_jsxs(XStack, { items: "center", gap: 4, "data-slot": "prompt-start", flex: 1, minW: 0, flexWrap: "wrap", children: [o.plus ? _jsx(Plus, { ...o.plus, disabled: disabled || o.plus.disabled }) : null, (o.on ?? []).map((c) => (_jsxs(XStack, { "data-slot": "prompt-chip", items: "center", gap: 6, pl: 8, pr: 4, height: 28, rounded: 8, bg: "$hover", maxW: 180, children: [c.icon ? _jsx(XStack, { "aria-hidden": true, children: c.icon }) : null, _jsx(SizableText, { fontSize: 13, color: "$ink", numberOfLines: 1, shrink: 1, children: c.label }), _jsx(XStack, { render: "button", "aria-label": `Remove ${c.label}`, onPress: c.onRemove, width: 20, height: 20, rounded: 6, items: "center", justify: "center", hoverStyle: { bg: '$raised' }, cursor: "pointer", children: _jsx(X, { size: 12, color: "$hint" }) })] }, c.id)))] })),
174
- send: (_jsxs(XStack, { items: "center", gap: 6, "data-slot": "prompt-end", shrink: 0, children: [o.voice ? _jsx(Dictation, { voice: o.voice, disabled: disabled, language: o.language ?? null, ...(o.onLanguage ? { onLanguage: o.onLanguage } : {}) }) : null, !busy && !lit && o.talk ? (_jsx(Control, { asChild: true, size: ROUND, fill: narrow, rounded: 999, ...(narrow ? null : QUIET), children: _jsx(Voice, { voice: o.talk, disabled: disabled, "data-slot": "composer-talk", says: { idle: 'Voice mode', listening: 'In conversation — click to hang up', speaking: 'Hanzo is speaking — talk to interrupt' }, children: () => _jsx(AudioLines, { size: 16 }) }) })) : (round)] })),
191
+ send: (_jsxs(XStack, { items: "center", gap: 6, "data-slot": "prompt-end", shrink: 0, children: [o.voice ? _jsx(Dictation, { voice: o.voice, disabled: disabled, narrow: narrow, language: o.language ?? null, ...(o.onLanguage ? { onLanguage: o.onLanguage } : {}) }) : null, !busy && !lit && o.talk ? (_jsx(Control, { asChild: true, size: narrow ? TAP : ROUND, fill: narrow, rounded: 999, ...(narrow ? null : QUIET), children: _jsx(Voice, { voice: o.talk, disabled: disabled, "data-slot": "composer-talk", says: { idle: 'Voice mode', listening: 'In conversation — click to hang up', speaking: 'Hanzo is speaking — talk to interrupt' }, children: () => _jsx(AudioLines, { size: 16, color: narrow ? ON : 'var(--foreground)' }) }) })) : (round)] })),
175
192
  };
176
193
  }
177
194
  /** Ctrl+U (⌘U) uploads, while a composer with an upload is on the page. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.61",
3
+ "version": "0.2.63",
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.",
@@ -14,7 +14,7 @@
14
14
  "@hanzo/iam": "0.21.28",
15
15
  "@hanzo/ui": "8.29.0",
16
16
  "@hanzo/vite": "0.1.4",
17
- "@hanzo/voice": "0.1.18",
17
+ "@hanzo/voice": "0.1.19",
18
18
  "@hanzogui/lucide-icons-2": "8.3.5",
19
19
  "@hanzogui/next-theme": "8.3.5",
20
20
  "@playwright/test": "1.63.0",
@@ -72,10 +72,10 @@
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@hanzo/gui": ">=8.3.5",
75
- "@hanzo/ui": ">=8.27.67",
75
+ "@hanzo/ui": ">=8.29.0",
76
76
  "@hanzogui/lucide-icons-2": ">=8.3.5",
77
77
  "react": ">=19.2.8",
78
- "@hanzo/voice": ">=0.1.16",
78
+ "@hanzo/voice": ">=0.1.19",
79
79
  "@hanzo/composer": ">=0.2.0"
80
80
  },
81
81
  "scripts": {