@hanzo/build 0.2.16 → 0.2.18

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.
Files changed (59) hide show
  1. package/lib/api/codebases.d.ts +1 -1
  2. package/lib/api/codebases.js +3 -3
  3. package/lib/api/harness.d.ts +7 -0
  4. package/lib/api/harness.js +34 -6
  5. package/lib/api/sessions.d.ts +2 -2
  6. package/lib/api/sessions.js +1 -1
  7. package/lib/api/turn.js +37 -16
  8. package/lib/ask.js +2 -2
  9. package/lib/builder.js +36 -4
  10. package/lib/choice.js +2 -2
  11. package/lib/customize/agents.js +4 -4
  12. package/lib/customize/connectors.js +7 -8
  13. package/lib/customize/index.js +3 -2
  14. package/lib/customize/plugins.js +3 -3
  15. package/lib/customize/skills.js +4 -4
  16. package/lib/customize/ui.d.ts +6 -2
  17. package/lib/customize/ui.js +9 -3
  18. package/lib/data.js +17 -15
  19. package/lib/desk.d.ts +4 -4
  20. package/lib/desk.js +6 -4
  21. package/lib/door.js +1 -1
  22. package/lib/environment.js +3 -3
  23. package/lib/find.js +2 -2
  24. package/lib/forge.js +6 -14
  25. package/lib/git.js +1 -1
  26. package/lib/host.d.ts +2 -0
  27. package/lib/landing.js +25 -16
  28. package/lib/markdown.js +6 -4
  29. package/lib/pitch.d.ts +4 -0
  30. package/lib/pitch.js +39 -0
  31. package/lib/plans.js +10 -9
  32. package/lib/project.js +20 -21
  33. package/lib/prose.js +1 -1
  34. package/lib/publish.js +9 -8
  35. package/lib/run.js +72 -45
  36. package/lib/settings/account.js +7 -5
  37. package/lib/settings/billing.js +11 -20
  38. package/lib/settings/capabilities.js +1 -1
  39. package/lib/settings/card.js +2 -2
  40. package/lib/settings/code.js +6 -3
  41. package/lib/settings/general.js +6 -5
  42. package/lib/settings/index.js +2 -3
  43. package/lib/settings/integrations.js +2 -2
  44. package/lib/settings/keys.js +8 -9
  45. package/lib/settings/machines.js +5 -9
  46. package/lib/settings/members.js +3 -2
  47. package/lib/settings/memory.js +1 -1
  48. package/lib/settings/notifications.js +1 -1
  49. package/lib/settings/privacy.js +2 -2
  50. package/lib/settings/usage.js +8 -9
  51. package/lib/shelf.js +5 -9
  52. package/lib/sync.js +2 -5
  53. package/lib/tools.js +2 -3
  54. package/lib/transcript.d.ts +5 -2
  55. package/lib/transcript.js +21 -3
  56. package/lib/visit.d.ts +31 -0
  57. package/lib/visit.js +53 -0
  58. package/lib/voice.js +3 -5
  59. package/package.json +1 -1
@@ -73,14 +73,14 @@ export function AddCard({ open, onOpenChange, onSaved }) {
73
73
  gone = true;
74
74
  const f = field.current;
75
75
  field.current = null;
76
+ setMounted(false);
76
77
  if (f)
77
78
  void f.destroy().catch(() => { });
78
79
  };
79
80
  }, [open, box, p]);
81
+ // Save is offered only while the field is drawn.
80
82
  const add = async () => {
81
83
  const f = field.current;
82
- if (!f)
83
- return;
84
84
  setWorking(true);
85
85
  setNote('');
86
86
  try {
@@ -32,7 +32,7 @@ export function Code() {
32
32
  const now = { model: ENSO, effort: 'medium', mode: 'build', place: '', ...prefs.code };
33
33
  const keep = (change) => {
34
34
  setNote('');
35
- save({ code: { ...prefs.code, ...change } }).catch((e) => setNote(e instanceof Error ? e.message : 'That was not saved'));
35
+ save({ code: { ...prefs.code, ...change } }).catch((e) => setNote(e.message));
36
36
  };
37
37
  const where = (places.value.length ? places.value : [SANDBOX]).map((p) => ({
38
38
  id: p.id || 'sandbox',
@@ -41,10 +41,13 @@ export function Code() {
41
41
  hint: p.id ? [p.status, p.capacity].filter(Boolean).join(' · ') : 'The platform’s own sandbox',
42
42
  place: p.id,
43
43
  }));
44
- const place = where.find((w) => w.place === now.place) ?? { id: now.place, label: now.place || 'Default', place: now.place };
44
+ // The sandbox is always listed, so a place not found is a machine that is not: it reads as its id.
45
+ const place = where.find((w) => w.place === now.place) ?? { id: now.place, label: now.place, place: now.place };
45
46
  const model = catalog.value.find((m) => m.id === now.model) ?? { id: now.model, label: now.model === ENSO ? 'Enso' : now.model };
46
47
  const go = (section) => host.go(path({ kind: 'settings', section }));
47
- 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, 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: EFFORTS.find((e) => e.id === now.effort).label, chosen: EFFORTS.find((e) => e.id === now.effort) ?? null, items: EFFORTS, onChange: (e) => keep({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: "Mode", detail: MODES.find((m) => m.id === now.mode).hint, trailing: _jsx(ChipSelect, { name: "Default mode", label: MODES.find((m) => m.id === now.mode).label, chosen: MODES.find((m) => m.id === now.mode) ?? null, 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 })] }));
48
+ const effort = EFFORTS.find((e) => e.id === now.effort);
49
+ const mode = MODES.find((m) => m.id === now.mode);
50
+ 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, 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 })] }));
48
51
  }
49
52
  function Link({ label, onPress }) {
50
53
  return (_jsx(YStack, { render: "button", "aria-label": `Open ${label}`, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onPress, children: _jsx(ChevronRight, { size: 16 }) }));
@@ -34,11 +34,12 @@ export function General() {
34
34
  }
35
35
  const keep = (patch) => {
36
36
  setNote('');
37
- save(patch).catch((e) => setNote(e instanceof Error ? e.message : 'That was not saved'));
37
+ save(patch).catch((e) => setNote(e.message));
38
38
  };
39
- const theme = prefs.theme ?? host.theme ?? 'dark';
40
- const text = prefs.text ?? 'medium';
41
- const motion = prefs.motion ?? 'system';
39
+ // Each choice is one of its list: a saved one was read against it, and a host's theme is one of THEMES.
40
+ const theme = THEMES.find((x) => x.id === (prefs.theme ?? host.theme ?? 'dark'));
41
+ const text = TEXTS.find((x) => x.id === (prefs.text ?? 'medium'));
42
+ const motion = MOTIONS.find((x) => x.id === (prefs.motion ?? 'system'));
42
43
  const language = LANGUAGES.find((l) => l.id === prefs.language) ?? LANGUAGES.find((l) => l.id === spoken());
43
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "General", detail: "How Hanzo Build looks and listens. Saved to your account, so it follows you." }), _jsx(Group, { title: "Appearance", children: _jsxs(Card, { children: [host.chooseTheme ? (_jsx(Row, { first: true, title: "Theme", detail: "System follows this device\u2019s light or dark setting.", trailing: _jsx(ChipSelect, { name: "Theme", label: THEMES.find((x) => x.id === theme).label, chosen: THEMES.find((x) => x.id === theme) ?? null, items: THEMES, onChange: (x) => keep({ theme: x.id }), placement: "bottom-end", width: 200 }) })) : null, _jsx(Row, { first: !host.chooseTheme, title: "Text size", detail: "The transcript, the composer and every screen.", trailing: _jsx(ChipSelect, { name: "Text size", label: TEXTS.find((x) => x.id === text).label, chosen: TEXTS.find((x) => x.id === text) ?? null, items: TEXTS, onChange: (x) => keep({ text: x.id === 'medium' ? null : x.id }), placement: "bottom-end", width: 200 }) }), _jsx(Row, { title: "Motion", detail: "Reduced stops animations and transitions on this page.", trailing: _jsx(ChipSelect, { name: "Motion", label: MOTIONS.find((x) => x.id === motion).label, chosen: MOTIONS.find((x) => x.id === motion) ?? null, items: MOTIONS, onChange: (x) => keep({ motion: x.id === 'system' ? null : x.id }), placement: "bottom-end", width: 220 }) })] }) }), _jsx(Group, { title: "Voice", children: _jsx(Card, { children: _jsx(Row, { first: true, title: "Dictation language", detail: "What the microphone in the composer listens for.", trailing: _jsx(ChipSelect, { name: "Dictation language", label: language.label, chosen: language, items: LANGUAGES, onChange: (l) => keep({ language: l.id }), placeholder: "Search languages\u2026", placement: "bottom-end", width: 220 }) }) }) }), _jsx(Note, { children: note })] }));
44
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "General", detail: "How Hanzo looks and listens. Saved to your account, so it follows you." }), _jsx(Group, { title: "Appearance", children: _jsxs(Card, { children: [host.chooseTheme ? (_jsx(Row, { first: true, title: "Theme", detail: "System follows this device\u2019s light or dark setting.", trailing: _jsx(ChipSelect, { name: "Theme", label: theme.label, chosen: theme, items: THEMES, onChange: (x) => keep({ theme: x.id }), placement: "bottom-end", width: 200 }) })) : null, _jsx(Row, { first: !host.chooseTheme, title: "Text size", detail: "The transcript, the composer and every screen.", trailing: _jsx(ChipSelect, { name: "Text size", label: text.label, chosen: text, items: TEXTS, onChange: (x) => keep({ text: x.id === 'medium' ? null : x.id }), placement: "bottom-end", width: 200 }) }), _jsx(Row, { title: "Motion", detail: "Reduced stops animations and transitions on this page.", trailing: _jsx(ChipSelect, { name: "Motion", label: motion.label, chosen: motion, items: MOTIONS, onChange: (x) => keep({ motion: x.id === 'system' ? null : x.id }), placement: "bottom-end", width: 220 }) })] }) }), _jsx(Group, { title: "Voice", children: _jsx(Card, { children: _jsx(Row, { first: true, title: "Dictation language", detail: "What the microphone in the composer listens for.", trailing: _jsx(ChipSelect, { name: "Dictation language", label: language.label, chosen: language, items: LANGUAGES, onChange: (l) => keep({ language: l.id }), placeholder: "Search languages\u2026", placement: "bottom-end", width: 220 }) }) }) }), _jsx(Note, { children: note })] }));
44
45
  }
@@ -11,12 +11,11 @@ import { path } from '../route.js';
11
11
  import { ENTRIES, GROUPS } from './sections.js';
12
12
  export function Settings({ section }) {
13
13
  const host = useHost();
14
- const entry = ENTRIES.find((e) => e.id === section) ?? ENTRIES[0];
14
+ // Every section the route grammar names has an entry.
15
+ const entry = ENTRIES.find((e) => e.id === section);
15
16
  const go = (id) => host.go(path({ kind: 'settings', section: id }));
16
17
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [_jsxs(YStack, { role: "navigation", "aria-label": "Settings", width: 220, shrink: 0, borderRightWidth: 1, borderColor: "$borderColor", px: "$2", py: "$4", gap: "$4", overflow: "scroll", "$max-md": { display: 'none' }, children: [_jsx(SizableText, { size: "$4", color: "$ink", px: "$2", children: "Settings" }), GROUPS.map((g) => {
17
18
  const list = ENTRIES.filter((e) => e.group === g);
18
- if (!list.length)
19
- return null;
20
19
  return (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g }), list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
21
20
  })] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
22
21
  }
@@ -39,7 +39,7 @@ export function Integrations() {
39
39
  setNote(done);
40
40
  }
41
41
  catch (e) {
42
- setNote(e instanceof Error ? e.message : 'That did not work');
42
+ setNote(e.message);
43
43
  }
44
44
  finally {
45
45
  setWorking(false);
@@ -53,7 +53,7 @@ export function Integrations() {
53
53
  host.open(await where());
54
54
  }
55
55
  catch (e) {
56
- setNote(e instanceof Error ? e.message : 'The connection did not start');
56
+ setNote(e.message);
57
57
  setWorking(false);
58
58
  }
59
59
  };
@@ -50,9 +50,9 @@ export function Keys() {
50
50
  }) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, rotating: making ? Boolean(held(making)) : false, onClose: () => setMaking(null), onMade: (m) => {
51
51
  setNote(`${NAME[m.type]} created`);
52
52
  list.reload();
53
- } }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: `Revoke your ${revoking ?? 'secret'} key?`, says: "It stops working within a minute, and anything using it is refused until it has a new one.", act: "Revoke", run: async () => {
54
- if (!revoking)
55
- return;
53
+ } }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: `Revoke your ${revoking ?? 'secret'} key?`, says: "It stops working within a minute, and anything using it is refused until it has a new one.", act: "Revoke",
54
+ // It acts only while it is open, which is while a type is chosen.
55
+ run: async () => {
56
56
  await revoke(t, revoking);
57
57
  setNote(`${NAME[revoking]} revoked`);
58
58
  list.reload();
@@ -73,16 +73,15 @@ function Making({ type, rotating, onClose, onMade }) {
73
73
  setNote('');
74
74
  onClose();
75
75
  };
76
- const go = async () => {
77
- if (!type)
78
- return;
76
+ // Pressed only in the open dialog, which is open only for a type.
77
+ const go = async (type) => {
79
78
  setNote('');
80
79
  let list;
81
80
  try {
82
81
  list = limits(limit);
83
82
  }
84
83
  catch (e) {
85
- setNote(e instanceof Error ? e.message : 'That is not a limit');
84
+ setNote(e.message);
86
85
  return;
87
86
  }
88
87
  setWorking(true);
@@ -92,11 +91,11 @@ function Making({ type, rotating, onClose, onMade }) {
92
91
  onMade(m);
93
92
  }
94
93
  catch (e) {
95
- setNote(e instanceof Error ? e.message : 'That did not work');
94
+ setNote(e.message);
96
95
  }
97
96
  finally {
98
97
  setWorking(false);
99
98
  }
100
99
  };
101
- return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? `Your new ${made.type} key` : `${rotating ? 'Rotate' : 'Create'} your ${type ?? ''} key` }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: "Secret key" })) : (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: made.key }), _jsx(CopyButton, { value: made.key, label: "Copy publishable key" })] })), made.limit.length ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["It reaches ", made.limit.join(', '), " and nothing else."] })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: "Done" }) })] })) : (_jsxs(YStack, { gap: "$3", children: [rotating ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["The ", type, " key you have now stops working."] })) : null, _jsx(Field, { label: "Limit", hint: "Optional. What it may reach, as kind:name \u2014 model:zen5, project:acme, product:commerce. Empty reaches whatever you do.", children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": "Limit", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: rotating ? 'Rotate' : 'Create' })] })] }))] }) }));
100
+ return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? `Your new ${made.type} key` : `${rotating ? 'Rotate' : 'Create'} your ${type ?? ''} key` }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: "Secret key" })) : (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: made.key }), _jsx(CopyButton, { value: made.key, label: "Copy publishable key" })] })), made.limit.length ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["It reaches ", made.limit.join(', '), " and nothing else."] })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: "Done" }) })] })) : (_jsxs(YStack, { gap: "$3", children: [rotating ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["The ", type, " key you have now stops working."] })) : null, _jsx(Field, { label: "Limit", hint: "Optional. What it may reach, as kind:name \u2014 model:zen5, project:acme, product:commerce. Empty reaches whatever you do.", children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": "Limit", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(type), children: rotating ? 'Rotate' : 'Create' })] })] }))] }) }));
102
101
  }
@@ -50,7 +50,7 @@ export function Machines() {
50
50
  list.reload();
51
51
  }
52
52
  catch (e) {
53
- setNote(e instanceof Error ? e.message : 'That did not work');
53
+ setNote(e.message);
54
54
  }
55
55
  };
56
56
  return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsx(Group, { title: "Linked", children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading machines\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { status: m.status }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
@@ -64,21 +64,17 @@ export function Machines() {
64
64
  ] }) }, m.id))) })) }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
65
65
  setNote(`${m.label} is registered`);
66
66
  list.reload();
67
- } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '', run: async (label) => {
68
- if (!renaming)
69
- return;
67
+ } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
68
+ // Each dialog acts only while it is open, which is while its machine is chosen.
69
+ run: async (label) => {
70
70
  await change(t, renaming.id, { label });
71
71
  setNote(`Renamed to ${label}`);
72
72
  list.reload();
73
73
  } }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: `Remove ${removing?.label ?? 'this machine'}?`, says: "Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.", act: "Remove", run: async () => {
74
- if (!removing)
75
- return;
76
74
  await remove(t, removing.id);
77
75
  setNote(`${removing.label} is removed`);
78
76
  list.reload();
79
77
  } }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
80
- if (!keying)
81
- return;
82
78
  const k = await key(t, keying.id);
83
79
  setMinted({ machine: keying, key: k });
84
80
  } }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
@@ -110,7 +106,7 @@ function Adding({ open, onOpenChange, onAdded }) {
110
106
  close();
111
107
  }
112
108
  catch (e) {
113
- setNote(e instanceof Error ? e.message : 'That did not work');
109
+ setNote(e.message);
114
110
  setWorking(false);
115
111
  }
116
112
  };
@@ -38,7 +38,7 @@ export function Members() {
38
38
  setNote(done);
39
39
  }
40
40
  catch (e) {
41
- setNote(e instanceof Error ? e.message : 'That did not work');
41
+ setNote(e.message);
42
42
  }
43
43
  finally {
44
44
  setWorking(false);
@@ -46,8 +46,9 @@ export function Members() {
46
46
  };
47
47
  const send = () => {
48
48
  const address = email.trim();
49
+ // Invite is offered only once something is typed.
49
50
  if (!EMAIL.test(address)) {
50
- setNote(`${address || 'That'} is not an email address`);
51
+ setNote(`${address} is not an email address`);
51
52
  return;
52
53
  }
53
54
  void act(() => invite(t, org, address), `${address} is invited. IAM sends no email: share the code with them.`, () => {
@@ -39,7 +39,7 @@ export function Memory() {
39
39
  return true;
40
40
  }
41
41
  catch (e) {
42
- setNote(e instanceof Error ? e.message : 'That did not work');
42
+ setNote(e.message);
43
43
  return false;
44
44
  }
45
45
  finally {
@@ -37,7 +37,7 @@ export function Notifications() {
37
37
  setNote(await what());
38
38
  }
39
39
  catch (e) {
40
- setNote(e instanceof Error ? e.message : 'That did not work');
40
+ setNote(e.message);
41
41
  }
42
42
  finally {
43
43
  setWorking(false);
@@ -33,7 +33,7 @@ export function Privacy() {
33
33
  setShown(await setConsent(t, change));
34
34
  }
35
35
  catch (e) {
36
- setNote(e instanceof Error ? e.message : 'That was not saved');
36
+ setNote(e.message);
37
37
  }
38
38
  finally {
39
39
  setBusy('');
@@ -48,7 +48,7 @@ export function Privacy() {
48
48
  setNote(`${saved.name} is ${saved.visibility || 'saved'}`);
49
49
  }
50
50
  catch (e) {
51
- setNote(e instanceof Error ? e.message : 'That was not saved');
51
+ setNote(e.message);
52
52
  }
53
53
  finally {
54
54
  setBusy('');
@@ -14,9 +14,9 @@ import { useHost, useTarget } from '../host.js';
14
14
  import { path } from '../route.js';
15
15
  import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
16
16
  const SPAN = { hour: 'This hour', day: 'Today', week: 'This week', month: 'This month' };
17
- /** A thin bar: how much of a whole is used. */
17
+ /** A thin bar: how much of a whole is used. Every whole drawn is above zero: a limit of none is not drawn. */
18
18
  function Bar({ part, whole, limit }) {
19
- const pct = whole > 0 ? Math.min(100, Math.max(0, (part / whole) * 100)) : 0;
19
+ const pct = Math.min(100, Math.max(0, (part / whole) * 100));
20
20
  return (_jsx(YStack, { height: 6, rounded: 999, bg: "$hover", overflow: "hidden", role: "progressbar", "aria-valuenow": Math.round(pct), "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx(YStack, { height: 6, width: `${pct}%`, rounded: 999, bg: limit && pct >= 100 ? '$red10' : '$ink' }) }));
21
21
  }
22
22
  /**
@@ -67,7 +67,7 @@ export function Usage() {
67
67
  setNote(done);
68
68
  }
69
69
  catch (e) {
70
- setNote(e instanceof Error ? e.message : 'That did not work');
70
+ setNote(e.message);
71
71
  }
72
72
  finally {
73
73
  setWorking(false);
@@ -80,15 +80,14 @@ export function Usage() {
80
80
  const others = ceilings.value.filter((c) => c !== cap);
81
81
  const s = spent.value;
82
82
  const top = Math.max(0, ...(s?.categories ?? []).map((c) => c.cents));
83
+ const adding = cents(amount);
84
+ // Offered only with a card to charge.
83
85
  const buy = () => {
84
- const c = cents(amount);
85
- if (!c) {
86
+ if (!adding) {
86
87
  setNote('Enter an amount in dollars');
87
88
  return;
88
89
  }
89
- if (!card)
90
- return;
91
- void act(() => topup(t, c, card.id), `${money(c)} is added to the balance`, () => {
90
+ void act(() => topup(t, adding, card.id), `${money(adding)} is added to the balance`, () => {
92
91
  setBuying(false);
93
92
  left.reload();
94
93
  });
@@ -104,7 +103,7 @@ export function Usage() {
104
103
  ceilings.reload();
105
104
  });
106
105
  };
107
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: cents(amount) ? `Add ${money(cents(amount) ?? 0)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
106
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
108
107
  .filter(Boolean)
109
108
  .join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
110
109
  }
package/lib/shelf.js CHANGED
@@ -42,7 +42,7 @@ function useFork() {
42
42
  host.go(made.slug || slug);
43
43
  }
44
44
  catch (e) {
45
- setFailed(e instanceof Error ? e.message : 'Could not copy this template');
45
+ setFailed(e.message);
46
46
  }
47
47
  finally {
48
48
  setBusy('');
@@ -93,7 +93,7 @@ export function Artifacts() {
93
93
  list.reload();
94
94
  }
95
95
  catch (e) {
96
- setNote(e instanceof Error ? e.message : 'That did not work');
96
+ setNote(e.message);
97
97
  }
98
98
  };
99
99
  const said = failed || note;
@@ -104,19 +104,15 @@ export function Artifacts() {
104
104
  { key: 'visibility', label: p.visibility === 'private' ? 'Make public' : 'Make private', onSelect: () => void flip(p) },
105
105
  { type: 'separator' },
106
106
  { key: 'delete', label: 'Delete', destructive: true, onSelect: () => setDeleting(p) },
107
- ] })] }, p.slug))) })] }, g.label))) }))] }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename", name: renaming?.name ?? '', run: async (name) => {
108
- if (!renaming)
109
- return;
107
+ ] })] }, p.slug))) })] }, g.label))) }))] }), renaming ? (_jsx(Rename, { open: true, onOpenChange: (o) => !o && setRenaming(null), title: "Rename", name: renaming.name, run: async (name) => {
110
108
  await change(t, renaming.slug, { name });
111
109
  setNote(`Renamed to ${name}`);
112
110
  list.reload();
113
- } }), _jsx(Confirm, { open: deleting !== null, onOpenChange: (o) => !o && setDeleting(null), title: `Delete ${deleting?.name ?? 'this project'}?`, says: "Its site stops answering, its releases are dropped, and its address is released. This cannot be undone.", act: "Delete", run: async () => {
114
- if (!deleting)
115
- return;
111
+ } })) : null, deleting ? (_jsx(Confirm, { open: true, onOpenChange: (o) => !o && setDeleting(null), title: `Delete ${deleting.name}?`, says: "Its site stops answering, its releases are dropped, and its address is released. This cannot be undone.", act: "Delete", run: async () => {
116
112
  await remove(t, deleting.slug);
117
113
  setNote(`${deleting.name} is deleted`);
118
114
  list.reload();
119
- } })] }));
115
+ } })) : null] }));
120
116
  }
121
117
  function Shot({ slug, height = 140 }) {
122
118
  const [gone, setGone] = useState(false);
package/lib/sync.js CHANGED
@@ -17,9 +17,6 @@ import { useRead } from './data.js';
17
17
  import { useHost, useTarget } from './host.js';
18
18
  import { path } from './route.js';
19
19
  const EMPTY = { repos: [], unread: [] };
20
- function noteOf(e, fallback) {
21
- return e instanceof Error ? e.message : fallback;
22
- }
23
20
  export function Sync() {
24
21
  const host = useHost();
25
22
  const t = useTarget();
@@ -57,7 +54,7 @@ export function Sync() {
57
54
  host.open(url);
58
55
  }
59
56
  catch (e) {
60
- setError(noteOf(e, 'Could not open the GitHub connection'));
57
+ setError(e.message);
61
58
  }
62
59
  };
63
60
  const send = async () => {
@@ -73,7 +70,7 @@ export function Sync() {
73
70
  setSyncing(true);
74
71
  }
75
72
  catch (e) {
76
- setError(noteOf(e, 'Could not queue those repositories'));
73
+ setError(e.message);
77
74
  }
78
75
  finally {
79
76
  setBusy(false);
package/lib/tools.js CHANGED
@@ -45,8 +45,7 @@ export function Attach({ files, onFiles, onNote }) {
45
45
  // `hidden` alone loses to a reset that sets `display` on inputs.
46
46
  style: { display: 'none' }, tabIndex: -1, "aria-hidden": true, onChange: (e) => {
47
47
  const input = e.currentTarget;
48
- if (!input.files)
49
- return;
48
+ // A file input always holds a list, empty or not.
50
49
  void read(files, input.files).then(({ files: next, note }) => {
51
50
  onFiles(next);
52
51
  onNote(note);
@@ -60,5 +59,5 @@ export function Files({ files, onFiles }) {
60
59
  }
61
60
  export function Dictate({ onText, onNote }) {
62
61
  const voice = useDictation(useTarget(), onText, onNote);
63
- return (_jsxs(XStack, { items: "center", children: [_jsx(ComposerTool, { label: !voice.able ? 'Dictation needs a microphone this page can record' : voice.on ? 'Stop and transcribe' : voice.busy ? 'Transcribing…' : 'Dictate', icon: voice.on ? _jsx(MicOff, { size: 14 }) : _jsx(Mic, { size: 14 }), onPress: () => void voice.toggle(), disabled: !voice.able || voice.busy, "aria-pressed": voice.on }), _jsx(ChipSelect, { quiet: true, name: "Dictation language", icon: _jsx(ChevronDown, { size: 12 }), label: "", chosen: voice.languages.find((l) => l.id === voice.language) ?? null, items: voice.languages, onChange: (l) => voice.setLanguage(l.id), placeholder: "Search languages\u2026", disabled: !voice.able })] }));
62
+ return (_jsxs(XStack, { items: "center", children: [_jsx(ComposerTool, { label: !voice.able ? 'Dictation needs a microphone this page can record' : voice.on ? 'Stop and transcribe' : voice.busy ? 'Transcribing…' : 'Dictate', icon: voice.on ? _jsx(MicOff, { size: 14 }) : _jsx(Mic, { size: 14 }), onPress: () => void voice.toggle(), disabled: !voice.able || voice.busy, "aria-pressed": voice.on }), _jsx(ChipSelect, { quiet: true, name: "Dictation language", icon: _jsx(ChevronDown, { size: 12 }), label: "", chosen: voice.languages.find((l) => l.id === voice.language), items: voice.languages, onChange: (l) => voice.setLanguage(l.id), placeholder: "Search languages\u2026", disabled: !voice.able })] }));
64
63
  }
@@ -1,6 +1,7 @@
1
- import type { ReactNode } from 'react';
1
+ import { type Verdict } from '@hanzo/ui/agents';
2
+ import { type ReactNode } from 'react';
2
3
  import type { Card } from './api/turn.ts';
3
- export declare function Transcript({ cards, live, empty, header, onApprove, approving, }: {
4
+ export declare function Transcript({ cards, live, empty, header, onApprove, approving, onVerdict, }: {
4
5
  cards: Card[];
5
6
  /** The run is still working: a card still running shows it; otherwise it was cut off. */
6
7
  live: boolean;
@@ -9,4 +10,6 @@ export declare function Transcript({ cards, live, empty, header, onApprove, appr
9
10
  /** Build what a plan says. Absent, a plan has no button. */
10
11
  onApprove?: (plan: string) => void;
11
12
  approving?: boolean;
13
+ /** Record a verdict on what the agent said; throws when it did not land. */
14
+ onVerdict: (v: Verdict) => Promise<void>;
12
15
  }): import("react").JSX.Element;
package/lib/transcript.js CHANGED
@@ -4,6 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * agent said as markdown, each command it ran as a card that opens onto its
5
5
  * output, the files it changed with their diff, the files it read as chips,
6
6
  * the run's own steps, and a plan run's plan with the button that builds it.
7
+ * What the agent said, and its plan, can be copied and judged, as a reply on
8
+ * claude.ai can: a thumb pressed again takes the verdict back.
7
9
  *
8
10
  * It follows the run to the bottom as it streams and stops when the reader
9
11
  * scrolls up (Thread). Everything drawn is text.
@@ -11,7 +13,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
12
14
  import { FileText } from '@hanzogui/lucide-icons-2';
13
15
  import { Button } from '@hanzo/ui';
16
+ import { Feedback } from '@hanzo/ui/agents';
14
17
  import { Code, Message, Step, Thread } from '@hanzo/ui/chat';
18
+ import { useState } from 'react';
15
19
  import { Patch } from './git.js';
16
20
  import { Prose } from './prose.js';
17
21
  /** A card's output, its tail: what a command said last is why it stopped. */
@@ -30,7 +34,7 @@ function rows(cards) {
30
34
  }
31
35
  return out;
32
36
  }
33
- export function Transcript({ cards, live, empty, header, onApprove, approving = false, }) {
37
+ export function Transcript({ cards, live, empty, header, onApprove, approving = false, onVerdict, }) {
34
38
  // A card the run never finished is cut off once the run has ended, not still going.
35
39
  const state = (ran) => (ran === 'running' && !live ? 'cancelled' : ran);
36
40
  return (_jsxs(Thread, { maxWidth: 0, gap: 14, column: { px: 0 }, "aria-label": "Transcript", children: [header, cards.length === 0 ? _jsx(YStack, { py: "$2", children: empty }) : null, rows(cards).map((row) => {
@@ -40,7 +44,7 @@ export function Transcript({ cards, live, empty, header, onApprove, approving =
40
44
  const c = row;
41
45
  switch (c.kind) {
42
46
  case 'said':
43
- return c.who === 'person' ? (_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$3", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: c.text }) }, c.key)) : (_jsx(Message, { role: "assistant", children: _jsx(Prose, { text: c.text }) }, c.key));
47
+ return c.who === 'person' ? (_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$3", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: c.text }) }, c.key)) : (_jsx(Message, { role: "assistant", actions: _jsx(Judge, { text: c.text, onVerdict: onVerdict }), children: _jsx(Prose, { text: c.text }) }, c.key));
44
48
  case 'shell':
45
49
  return (_jsx(Step, { name: "Shell", detail: c.command, status: state(c.ran), "aria-label": `Command ${c.command}`, children: _jsx(Code, { language: "shell", value: c.output ? `$ ${c.command}\n${c.output}` : `$ ${c.command}`, children: c.output ? `$ ${c.command}\n${tail(c.output)}` : `$ ${c.command}` }) }, c.key));
46
50
  case 'edit':
@@ -50,7 +54,21 @@ export function Transcript({ cards, live, empty, header, onApprove, approving =
50
54
  case 'note':
51
55
  return (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: c.text }, c.key));
52
56
  case 'plan':
53
- return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }), onApprove ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' }) })) : null] }, c.key));
57
+ return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }), _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [onApprove ? (_jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' })) : null, _jsx(Judge, { text: c.text, onVerdict: onVerdict })] })] }, c.key));
54
58
  }
55
59
  })] }));
56
60
  }
61
+ /** Copy what the agent said, and say whether it was good. A verdict that did not land is taken back and says why. */
62
+ function Judge({ text, onVerdict }) {
63
+ const [verdict, setVerdict] = useState(null);
64
+ const [note, setNote] = useState('');
65
+ const judge = (next) => {
66
+ setVerdict(next);
67
+ setNote('');
68
+ onVerdict(next).catch((e) => {
69
+ setVerdict(null);
70
+ setNote(e.message);
71
+ });
72
+ };
73
+ return (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Feedback, { text: text, verdict: verdict, onVerdict: judge }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }));
74
+ }
package/lib/visit.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const PLACES: readonly [{
3
+ readonly label: 'Download';
4
+ readonly href: '/#features';
5
+ }, {
6
+ readonly label: 'Enterprise';
7
+ readonly href: "https://hanzo.ai/enterprise";
8
+ }, {
9
+ readonly label: 'Pricing';
10
+ readonly href: "https://hanzo.ai/pricing";
11
+ }];
12
+ export declare const RESOURCES: readonly [{
13
+ readonly label: 'Docs';
14
+ readonly href: 'https://docs.hanzo.ai';
15
+ }, {
16
+ readonly label: 'Blog';
17
+ readonly href: "https://hanzo.ai/blog";
18
+ }, {
19
+ readonly label: 'Customers';
20
+ readonly href: "https://hanzo.ai/customers";
21
+ }, {
22
+ readonly label: 'Learn';
23
+ readonly href: "https://hanzo.ai/learn";
24
+ }, {
25
+ readonly label: 'Support';
26
+ readonly href: "https://hanzo.ai/support";
27
+ }];
28
+ /** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
29
+ export declare function Bar({ name }: {
30
+ name: ReactNode;
31
+ }): import("react").JSX.Element;
package/lib/visit.js ADDED
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The page to a visitor: the product's header over New.
4
+ *
5
+ * Signed out there are no runs to list, so the rail gives way to a header —
6
+ * Download (Hanzo Dev, under New, pitch.tsx), Resources, Enterprise, Pricing — and the two
7
+ * doors in, which are one door: IAM's page offers both. New stays the page, and
8
+ * sending from it asks the visitor to sign in with the sentence kept
9
+ * (landing.tsx). Below md the four places fold into one menu beside Log in.
10
+ */
11
+ import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
12
+ import { ChevronDown, Menu } from '@hanzogui/lucide-icons-2';
13
+ import { Button } from '@hanzo/ui';
14
+ import { MenuLabel, MenuRow, MenuRule } from '@hanzo/ui/product';
15
+ import { useState } from 'react';
16
+ import { useHost } from './host.js';
17
+ const SITE = 'https://hanzo.ai';
18
+ export const PLACES = [
19
+ { label: 'Download', href: '/#features' },
20
+ { label: 'Enterprise', href: `${SITE}/enterprise` },
21
+ { label: 'Pricing', href: `${SITE}/pricing` },
22
+ ];
23
+ export const RESOURCES = [
24
+ { label: 'Docs', href: 'https://docs.hanzo.ai' },
25
+ { label: 'Blog', href: `${SITE}/blog` },
26
+ { label: 'Customers', href: `${SITE}/customers` },
27
+ { label: 'Learn', href: `${SITE}/learn` },
28
+ { label: 'Support', href: `${SITE}/support` },
29
+ ];
30
+ const leave = (href) => window.location.assign(href);
31
+ /** One place in the header: an address, drawn as text. */
32
+ function Place({ href, children }) {
33
+ return (_jsx("a", { href: href, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: children }) }) }));
34
+ }
35
+ function Resources() {
36
+ const [open, setOpen] = useState(false);
37
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-start", offset: 6, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs(XStack, { render: "button", "aria-label": "Resources", "aria-expanded": open, items: "center", gap: "$1", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Resources" }), _jsx(ChevronDown, { size: 14, color: "$soft" })] }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Resources", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 220, bg: "$panel", borderColor: "$borderColor", children: _jsx(YStack, { gap: "$1", children: RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))) }) })] }));
38
+ }
39
+ /** Below md: every place, the resources and the door, in one menu. */
40
+ function Fold({ onEnter }) {
41
+ const [open, setOpen] = useState(false);
42
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-end", offset: 6, stayInFrame: true, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Menu", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Menu", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [PLACES.map((p) => (_jsx(MenuRow, { label: p.label, onPress: () => leave(p.href) }, p.label))), _jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Resources" }), RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Sign up", onPress: () => {
43
+ setOpen(false);
44
+ onEnter();
45
+ } })] }) })] }));
46
+ }
47
+ /** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
48
+ export function Bar({ name }) {
49
+ const host = useHost();
50
+ const enter = () => host.signIn?.();
51
+ const [features, enterprise, pricing] = PLACES;
52
+ return (_jsxs(XStack, { role: "banner", height: 60, px: "$4", gap: "$2", items: "center", shrink: 0, $md: { px: '$6' }, children: [name, _jsxs(XStack, { role: "navigation", "aria-label": "Site", items: "center", gap: "$1", ml: "$4", "$max-md": { display: 'none' }, children: [_jsx(Place, { href: features.href, children: features.label }), _jsx(Resources, {}), _jsx(Place, { href: enterprise.href, children: enterprise.label }), _jsx(Place, { href: pricing.href, children: pricing.label })] }), _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "sm", onPress: enter, children: "Log in" }), _jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Button, { size: "sm", variant: "primary", onPress: enter, children: "Sign up" }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Fold, { onEnter: enter }) })] }));
53
+ }