@hanzo/build 0.2.17 → 0.2.19

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/plans.js CHANGED
@@ -37,6 +37,7 @@ export function Plans() {
37
37
  const [working, setWorking] = useState(false);
38
38
  const [note, setNote] = useState('');
39
39
  const on = current(subs.value);
40
+ const who = host.org ?? 'This organization';
40
41
  const card = chosen(cards.value);
41
42
  const yearly = list.value.some((p) => p.yearly > 0);
42
43
  const free = (p) => p.monthly === 0 && !p.sales;
@@ -51,7 +52,7 @@ export function Plans() {
51
52
  subs.reload();
52
53
  }
53
54
  catch (e) {
54
- setNote(e instanceof Error ? e.message : 'That did not work');
55
+ setNote(e.message);
55
56
  }
56
57
  finally {
57
58
  setWorking(false);
@@ -73,10 +74,10 @@ export function Plans() {
73
74
  if (!settled)
74
75
  return null;
75
76
  if (free(p)) {
76
- if (!on)
77
- return null;
78
- if (on.ending)
79
- return _jsx(Line, { children: `You move to ${p.name} when ${on.name} ends on ${day(on.ends)}.` });
77
+ // A free plan that is not the org's means a paid one is.
78
+ const paid = on;
79
+ if (paid.ending)
80
+ return _jsx(Line, { children: `You move to ${p.name} when ${paid.name} ends on ${day(paid.ends)}.` });
80
81
  return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: `Switch to ${p.name}` }));
81
82
  }
82
83
  if (on && on.price > 0)
@@ -89,17 +90,17 @@ export function Plans() {
89
90
  return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 1100, mx: "auto", px: "$5", py: "$5", gap: "$5", children: [_jsxs(XStack, { render: "button", "aria-label": "Back to billing", items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Billing" })] }), _jsxs(XStack, { items: "flex-end", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 240, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", children: "Plans that grow with you" }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
90
91
  ? 'Sign in to see the plan you are on.'
91
92
  : on
92
- ? `${host.org ?? 'This organization'} is on ${on.name}.`
93
- : `${host.org ?? 'This organization'} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
93
+ ? `${who} is on ${on.name}.`
94
+ : `${who} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
94
95
  const cost = price(p, term);
95
96
  return (_jsxs(YStack, { "aria-label": `${p.name} plan`, flex: 1, minW: 240, maxW: 360, "$max-sm": { maxW: '100%' }, gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: mine(p) || p.popular ? '$ink' : '$borderColor', bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$5", color: "$ink", children: p.name }), mine(p) ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Current" })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Popular" })) : null] }), p.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: p.description })) : null, _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$7", color: "$ink", children: p.sales ? 'Custom' : cost.month === 0 ? 'Free' : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
96
97
  ? 'Talk to us'
97
98
  : cost.month === 0
98
99
  ? 'Forever'
99
100
  : `${p.perSeat ? 'per seat, ' : ''}a month${term === 'year' && p.yearly ? `, ${money(cost.now, p.currency)} billed yearly` : ''}` })] }), _jsx(XStack, { children: move(p) }), p.features.length ? (_jsx(YStack, { gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: p.features.map((f) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(YStack, { pt: 2, children: _jsx(Check, { size: 14 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$soft", children: f })] }, f))) })) : null] }, p.id));
100
- }) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => (o ? undefined : setBuying(null)), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
101
+ }) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => !o && setBuying(null), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
101
102
  await subscribe(t, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
102
- return `${host.org ?? 'This organization'} is on ${buying.name}`;
103
+ return `${who} is on ${buying.name}`;
103
104
  }), children: "Upgrade" })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Switch to the free plan?" }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${on.name} stays on until ${day(on.ends) || 'the end of this period'}, and nothing is charged after that.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: `Keep ${on.name}` }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
104
105
  await cancel(t, on.id);
105
106
  return `${on.name} ends on ${day(on.ends)}`;
package/lib/project.js CHANGED
@@ -26,7 +26,7 @@ import { Composer, Message } from '@hanzo/ui/chat';
26
26
  import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
27
27
  import { HanzoMark } from '@hanzo/ui/product';
28
28
  import { useEffect, useMemo, useRef, useState } from 'react';
29
- import { start, unhonoured } from './api/coding.js';
29
+ import { start } from './api/coding.js';
30
30
  import { blob, tree } from './api/git.js';
31
31
  import { name as repoName, ours } from './api/projects.js';
32
32
  import { list, message, stop } from './api/sessions.js';
@@ -48,7 +48,7 @@ function Turn({ run, open, onOpen, project }) {
48
48
  setV(next);
49
49
  record(t, run.id, project, next).catch((e) => {
50
50
  setV(null);
51
- setNote(e instanceof Error ? e.message : 'Not recorded');
51
+ setNote(e.message);
52
52
  });
53
53
  } }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: note })) : null, !open ? (_jsx(XStack, { render: "button", onPress: onOpen, hitSlop: 8, children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Show steps" }) })) : null] }), children: _jsx(SizableText, { size: "$2", color: "$ink", children: run.status === 'done' ? 'Done.' : run.status === 'error' ? 'This run hit an error.' : run.status === 'stopped' ? 'Stopped.' : 'Working…' }) })] }));
54
54
  }
@@ -113,7 +113,7 @@ export function Project({ slug }) {
113
113
  setFiles((was) => was.map((f) => (f.path === path ? { ...f, content, error } : f)));
114
114
  }
115
115
  catch (e) {
116
- setFiles((was) => was.map((f) => (f.path === path ? { ...f, error: e instanceof Error ? e.message : 'Could not read this file' } : f)));
116
+ setFiles((was) => was.map((f) => (f.path === path ? { ...f, error: e.message } : f)));
117
117
  }
118
118
  };
119
119
  // Pages: the HTML files at the repository's root, as the page picker's list.
@@ -148,21 +148,19 @@ export function Project({ slug }) {
148
148
  // The whole selector, as it will ride the next ask — never a shortened name
149
149
  // that hides what the page sent.
150
150
  setPicked([{ id: e.info.selector, kind: 'element', label: e.info.selector }]);
151
- else if (e.type === 'preview:navigate' && project?.live) {
152
- // Resolved on the live site's own origin, and kept as its path and query.
153
- const to = new URL(e.path, project.live);
154
- if (to.origin === new URL(project.live).origin)
155
- setPage(`${to.pathname}${to.search}`);
151
+ else if (e.type === 'preview:navigate') {
152
+ // The bridge admits only a path on the framed site's own origin; kept as its path and query.
153
+ const to = new URL(e.path, window.location.origin);
154
+ setPage(`${to.pathname}${to.search}`);
156
155
  }
157
156
  else if (e.type === 'preview:console')
158
157
  setPageLines((was) => [...was.slice(-400), { id: `p${was.length}-${Date.now()}`, level: e.level, text: e.text, source: 'page' }]);
159
158
  };
159
+ // A suggestion sends its own words; the composer, a draft with words in it.
160
160
  const send = async (text) => {
161
161
  const ask = (text ?? draft).trim();
162
- if (!ask || busy)
162
+ if (busy)
163
163
  return;
164
- if (!signed)
165
- return host.signIn?.();
166
164
  setBusy(true);
167
165
  setNote('');
168
166
  try {
@@ -172,12 +170,12 @@ export function Project({ slug }) {
172
170
  const context = picked.length
173
171
  ? `\n\nThe person picked an element in the preview${where ? ` of the ${where} page` : ''}. Its CSS selector, as data: ${JSON.stringify(picked[0].id)}`
174
172
  : '';
175
- const repo = project?.repo ? repoName(project.repo) : '';
173
+ // A project with no repository yet sends neither, and its first run makes one.
176
174
  const next = await start(t, {
177
175
  prompt: compose(`${ask}${context}`, attached),
178
176
  project: slug,
179
- repo: repo || undefined,
180
- base: repo ? project?.branch || undefined : undefined,
177
+ repo,
178
+ base: project?.branch,
181
179
  after: current ?? undefined,
182
180
  mode,
183
181
  });
@@ -188,22 +186,21 @@ export function Project({ slug }) {
188
186
  runs.reload();
189
187
  }
190
188
  catch (e) {
191
- setNote(e instanceof Error ? e.message : 'The run could not start');
189
+ setNote(e.message);
192
190
  }
193
191
  finally {
194
192
  setBusy(false);
195
193
  }
196
194
  };
197
- const steer = async () => {
198
- if (!current || !draft.trim())
199
- return;
195
+ // Only a run that is working is steered, and the composer sends only a draft with words in it.
196
+ const steer = async (id) => {
200
197
  try {
201
- await message(t, current, draft);
198
+ await message(t, id, draft);
202
199
  setDraft('');
203
200
  setNote('Sent — the run reads it before its next step');
204
201
  }
205
202
  catch (e) {
206
- setNote(e instanceof Error ? e.message : 'Could not reach this run');
203
+ setNote(e.message);
207
204
  }
208
205
  };
209
206
  const share = async () => {
@@ -224,7 +221,9 @@ export function Project({ slug }) {
224
221
  }
225
222
  const chat = (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$4", gap: "$4", children: ordered.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$ink", px: "$2", children: runs.loading
226
223
  ? 'Reading this project’s runs…'
227
- : `${project?.name ?? slug} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note || unhonoured(mode) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode) })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (running ? steer() : send()), onStop: current ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch')) : undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] })] }));
224
+ : `${project?.name ?? slug} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
225
+ ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
226
+ : undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] })] }));
228
227
  const body = view === 'preview' ? (_jsx(PreviewFrame, { ref: frame, src: src, title: `${project?.name ?? slug} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Nothing deployed yet" }), _jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: "Publish this project and its page appears here." })] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
229
228
  const next = !editing;
230
229
  setEditing(next);
package/lib/prose.js CHANGED
@@ -32,7 +32,7 @@ const SIZES = ['$6', '$5', '$4', '$4', '$3', '$3'];
32
32
  function Draw({ block }) {
33
33
  switch (block.kind) {
34
34
  case 'heading':
35
- return (_jsx(SizableText, { size: SIZES[block.level - 1] ?? '$3', color: "$ink", fontWeight: "600", pt: "$1", children: _jsx(Inline, { text: block.text }) }));
35
+ return (_jsx(SizableText, { size: SIZES[block.level - 1], color: "$ink", fontWeight: "600", pt: "$1", children: _jsx(Inline, { text: block.text }) }));
36
36
  case 'code':
37
37
  return (_jsx(Code, { language: block.lang || 'text', value: block.text, children: block.text }));
38
38
  case 'rule':
package/lib/publish.js CHANGED
@@ -69,26 +69,27 @@ export function Publish({ source, onClose }) {
69
69
  setPhase('declared');
70
70
  }
71
71
  catch (e) {
72
- setError(e instanceof Error ? e.message : 'The platform refused this');
72
+ setError(e.message);
73
73
  setPhase('failed');
74
74
  }
75
75
  };
76
+ // The build this write started; its status is what the platform last said of it.
77
+ const built = out?.build?.id ?? '';
78
+ const green = /^(succeeded|success|done|complete|completed|ok)$/i.test(status);
79
+ // Offered only once that build is green.
76
80
  const ship = async () => {
77
- if (!out?.build?.id)
78
- return;
79
81
  setPhase('shipping');
80
82
  setError('');
81
83
  try {
82
- await declare(t, { repo: source.repo, ref: source.ref, name: source.name, project: project || source.name, mode: 'commit', tag: out.build.id });
84
+ await declare(t, { repo: source.repo, ref: source.ref, name: source.name, project: project || source.name, mode: 'commit', tag: built });
83
85
  setPhase('shipped');
84
86
  }
85
87
  catch (e) {
86
- setError(e instanceof Error ? e.message : 'The platform refused this');
88
+ setError(e.message);
87
89
  setPhase('declared');
88
90
  }
89
91
  };
90
- const green = /^(succeeded|success|done|complete|completed|ok)$/i.test(status);
91
- return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Add to project" }), _jsxs(DialogDescription, { children: ["Build ", source.title, " at ", source.ref || 'main', " and declare it in a project. ", host.admin ? 'As an admin you can take a green build to main.' : 'It opens a review; merging it is what deploys.'] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { render: "label", size: "$2", color: "$soft", htmlFor: "publish-project", children: "Project" }), _jsx(Input, { id: "publish-project", value: project, onChangeText: (v) => setProject(v), disabled: phase !== 'ask' && phase !== 'failed', placeholder: label(source.name), "aria-label": "Project" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Saved as ", label(project || source.name)] })] }), out ? (_jsxs(YStack, { gap: "$2", role: "status", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: phase === 'shipped'
92
+ return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Add to project" }), _jsxs(DialogDescription, { children: ["Build ", source.title, " at ", source.ref, " and declare it in a project. ", host.admin ? 'As an admin you can take a green build to main.' : 'It opens a review; merging it is what deploys.'] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { render: "label", size: "$2", color: "$soft", htmlFor: "publish-project", children: "Project" }), _jsx(Input, { id: "publish-project", value: project, onChangeText: (v) => setProject(v), disabled: phase !== 'ask' && phase !== 'failed', placeholder: label(source.name), "aria-label": "Project" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Saved as ", label(project || source.name)] })] }), out ? (_jsxs(YStack, { gap: "$2", role: "status", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: phase === 'shipped'
92
93
  ? 'Declared on main — CD applies it on its next pass.'
93
- : `Build ${out.build?.id ?? ''}: ${status || 'building'}` }), out.review ? (_jsxs(Out, { href: out.review, children: [_jsx(SizableText, { size: "$2", color: "$ink", textDecorationLine: "underline", children: "Open the review" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] })) : null] })) : null, error ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: error })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { variant: "outline", size: "sm", onPress: onClose, children: phase === 'shipped' || phase === 'declared' ? 'Done' : 'Cancel' }), phase === 'ask' || phase === 'failed' || phase === 'sending' ? (_jsx(Button, { size: "sm", disabled: phase === 'sending', onPress: () => void send(), children: phase === 'sending' ? 'Adding…' : 'Add to project' })) : host.admin && (phase === 'declared' || phase === 'shipping') ? (_jsx(Button, { size: "sm", disabled: !green || phase === 'shipping', onPress: () => void ship(), children: phase === 'shipping' ? 'Shipping…' : green ? 'Ship to main' : 'Waiting for the build' })) : null] })] }) }));
94
+ : `Build ${built}: ${status || 'building'}` }), out.review ? (_jsxs(Out, { href: out.review, children: [_jsx(SizableText, { size: "$2", color: "$ink", textDecorationLine: "underline", children: "Open the review" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] })) : null] })) : null, error ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: error })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { variant: "outline", size: "sm", onPress: onClose, children: phase === 'shipped' || phase === 'declared' ? 'Done' : 'Cancel' }), phase === 'ask' || phase === 'failed' || phase === 'sending' ? (_jsx(Button, { size: "sm", disabled: phase === 'sending', onPress: () => void send(), children: phase === 'sending' ? 'Adding…' : 'Add to project' })) : host.admin && (phase === 'declared' || phase === 'shipping') ? (_jsx(Button, { size: "sm", disabled: !green || phase === 'shipping', onPress: () => void ship(), children: phase === 'shipping' ? 'Shipping…' : green ? 'Ship to main' : 'Waiting for the build' })) : null] })] }) }));
94
95
  }
@@ -62,8 +62,9 @@ export function Account() {
62
62
  const patch = {};
63
63
  if (callName.trim() !== (prefs.callName ?? ''))
64
64
  patch.callName = callName.trim() || null;
65
+ // A work is only ever chosen from the list, never emptied.
65
66
  if (work !== (prefs.work ?? ''))
66
- patch.work = work || null;
67
+ patch.work = work;
67
68
  if (instructions.trim() !== (prefs.instructions ?? '').trim())
68
69
  patch.instructions = instructions.trim() || null;
69
70
  const renamed = name.trim() !== shownName.trim();
@@ -79,7 +80,7 @@ export function Account() {
79
80
  setNote('Saved');
80
81
  }
81
82
  catch (e) {
82
- setNote(e instanceof Error ? e.message : 'That was not saved');
83
+ setNote(e.message);
83
84
  }
84
85
  finally {
85
86
  setBusy(false);
@@ -93,13 +94,14 @@ export function Account() {
93
94
  setNote('Your photo is saved');
94
95
  }
95
96
  catch (e) {
96
- setNote(e instanceof Error ? e.message : 'The photo was not saved');
97
+ setNote(e.message);
97
98
  }
98
99
  finally {
99
100
  setBusy(false);
100
101
  }
101
102
  };
102
- const orgs = host.memberships ?? (host.org ? [host.org] : []);
103
+ const org = host.org;
104
+ const orgs = host.memberships ?? (org ? [org] : []);
103
105
  const chosenWork = WORK.find((w) => w.id === work) ?? (work ? { id: work, label: work } : null);
104
106
  return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Account", detail: "Your Hanzo identity, and what Hanzo should know about you.", action: host.signOut ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Log out"] })) : undefined }), _jsx(Group, { title: "Profile", children: _jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [face ? (_jsx("img", { src: face, alt: "", width: 48, height: 48, style: { borderRadius: 999, objectFit: 'cover' } })) : (_jsx(YStack, { width: 48, height: 48, rounded: 999, bg: "$raised", items: "center", justify: "center", children: _jsx(SizableText, { size: "$5", color: "$ink", children: (shownName || who.email || '?').charAt(0).toUpperCase() }) })), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$4", color: "$ink", numberOfLines: 1, children: shownName || who.email }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => picker.current?.click(), children: "Change photo" }), _jsx("input", { ref: picker, type: "file", accept: PHOTO, hidden: true, style: { display: 'none' }, tabIndex: -1, "aria-label": "Photo", onChange: (e) => {
105
107
  const input = e.currentTarget;
@@ -107,5 +109,5 @@ export function Account() {
107
109
  input.value = '';
108
110
  if (file)
109
111
  void upload(file);
110
- } })] }), _jsx(Field, { label: "Full name", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Full name", placeholder: "Your name" }) }), _jsx(Field, { label: "Email", hint: "You sign in with this address, so it is not changed here.", children: _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email || 'No email on this account' }) }), _jsx(Field, { label: "What should Hanzo call you?", children: _jsx(Input, { value: callName, onChangeText: setCallName, "aria-label": "What should Hanzo call you?", placeholder: shownName.split(' ')[0] || 'A first name' }) }), _jsx(Field, { label: "What best describes your work?", children: _jsx(XStack, { children: _jsx(ChipSelect, { name: "Work", label: chosenWork?.label ?? 'Choose one', chosen: chosenWork, items: WORK, onChange: (w) => setWork(w.id), placement: "bottom-start", width: 260 }) }) }), _jsx(Field, { label: "Instructions for Hanzo", hint: `Every coding run reads these before it starts. ${instructions.length}/${INSTRUCTIONS}`, children: _jsx(Textarea, { value: instructions, onChangeText: (v) => setInstructions(v.slice(0, INSTRUCTIONS)), "aria-label": "Instructions for Hanzo", placeholder: "How you like to work: the languages and tools you prefer, the style to follow, what to avoid.", rows: 5 }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(Button, { size: "sm", disabled: busy || !changed, onPress: () => void submit(), children: "Save" }), _jsx(Note, { children: note })] })] }) }), host.org ? (_jsx(Group, { title: "Organization", detail: "The organization this page acts in. Runs, codebases and settings belong to it.", children: _jsx(Card, { children: _jsx(Row, { first: true, mono: true, title: host.org, detail: `Organization ID${host.admin ? ' · you are an admin' : ''}`, trailing: _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": copied ? 'Organization ID copied' : 'Copy organization ID', onPress: () => void navigator.clipboard?.writeText(host.org ?? '').then(() => setCopied(true)), children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }) }) }) })) : null, _jsx(Group, { title: "Organizations", detail: "Every organization you belong to. Switch to act in another.", children: orgs.length ? (_jsx(Card, { children: orgs.map((o, i) => (_jsx(Row, { first: i === 0, title: o, detail: o === host.org ? `Acting in ${o}${host.admin ? ' as an admin' : ''}` : undefined, trailing: o === host.org ? (_jsx(Check, { size: 16 })) : host.chooseOrg ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.chooseOrg?.(o), children: "Switch" })) : null }, o))) })) : (_jsx(Soft, { children: "You belong to no organization yet." })) })] }));
112
+ } })] }), _jsx(Field, { label: "Full name", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Full name", placeholder: "Your name" }) }), _jsx(Field, { label: "Email", hint: "You sign in with this address, so it is not changed here.", children: _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email || 'No email on this account' }) }), _jsx(Field, { label: "What should Hanzo call you?", children: _jsx(Input, { value: callName, onChangeText: setCallName, "aria-label": "What should Hanzo call you?", placeholder: shownName.split(' ')[0] || 'A first name' }) }), _jsx(Field, { label: "What best describes your work?", children: _jsx(XStack, { children: _jsx(ChipSelect, { name: "Work", label: chosenWork?.label ?? 'Choose one', chosen: chosenWork, items: WORK, onChange: (w) => setWork(w.id), placement: "bottom-start", width: 260 }) }) }), _jsx(Field, { label: "Instructions for Hanzo", hint: `Every coding run reads these before it starts. ${instructions.length}/${INSTRUCTIONS}`, children: _jsx(Textarea, { value: instructions, onChangeText: (v) => setInstructions(v.slice(0, INSTRUCTIONS)), "aria-label": "Instructions for Hanzo", placeholder: "How you like to work: the languages and tools you prefer, the style to follow, what to avoid.", rows: 5 }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(Button, { size: "sm", disabled: busy || !changed, onPress: () => void submit(), children: "Save" }), _jsx(Note, { children: note })] })] }) }), org ? (_jsx(Group, { title: "Organization", detail: "The organization this page acts in. Runs, codebases and settings belong to it.", children: _jsx(Card, { children: _jsx(Row, { first: true, mono: true, title: org, detail: `Organization ID${host.admin ? ' · you are an admin' : ''}`, trailing: _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": copied ? 'Organization ID copied' : 'Copy organization ID', onPress: () => void navigator.clipboard?.writeText(org).then(() => setCopied(true)), children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }) }) }) })) : null, _jsx(Group, { title: "Organizations", detail: "Every organization you belong to. Switch to act in another.", children: orgs.length ? (_jsx(Card, { children: orgs.map((o, i) => (_jsx(Row, { first: i === 0, title: o, detail: o === host.org ? `Acting in ${o}${host.admin ? ' as an admin' : ''}` : undefined, trailing: o === host.org ? (_jsx(Check, { size: 16 })) : host.chooseOrg ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.chooseOrg?.(o), children: "Switch" })) : null }, o))) })) : (_jsx(Soft, { children: "You belong to no organization yet." })) })] }));
111
113
  }
@@ -39,7 +39,7 @@ export function Billing() {
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);
@@ -52,14 +52,10 @@ export function Billing() {
52
52
  : 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsx(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : cards.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No card on file." }) })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [m.expires ? `Expires ${m.expires}` : '', m.default ? 'Default' : ''].filter(Boolean).join(' · ') || undefined, trailing: host.admin ? (_jsx(XStack, { render: "button", "aria-label": `Remove ${label(m)}`, p: "$1", onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: _jsx(X, { size: 14 }) })) : undefined }, m.id))) })) }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
53
53
  cards.reload();
54
54
  setNote(`${label(m)} is saved`);
55
- } }), _jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => {
56
- if (!plan)
57
- return;
58
- void act(() => cancel(t, plan.id), `Your plan ends on ${day(plan.ends)}`, () => {
59
- setEnding(false);
60
- subs.reload();
61
- });
62
- } }), _jsx(Bill, { invoice: open, onClose: () => setOpen(null) })] }));
55
+ } }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(t, plan.id), `Your plan ends on ${day(plan.ends)}`, () => {
56
+ setEnding(false);
57
+ subs.reload();
58
+ }) })) : null, open ? _jsx(Bill, { invoice: open, onClose: () => setOpen(null) }) : null] }));
63
59
  }
64
60
  /** What the plan costs, and when it renews or ends. */
65
61
  function terms(p) {
@@ -68,19 +64,14 @@ function terms(p) {
68
64
  }
69
65
  const word = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : '');
70
66
  function Ending({ plan, open, working, onOpenChange, onConfirm, }) {
71
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Cancel your plan?" }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: plan
72
- ? `${plan.name} stays on until ${day(plan.ends) || 'the end of this period'}, and nothing is charged after that. You can keep it any time before then.`
73
- : '' }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Keep plan" }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: onConfirm, children: "Cancel plan" })] })] })] }) }));
67
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Cancel your plan?" }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${plan.name} stays on until ${day(plan.ends) || 'the end of this period'}, and nothing is charged after that. You can keep it any time before then.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Keep plan" }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: onConfirm, children: "Cancel plan" })] })] })] }) }));
74
68
  }
75
- /** One invoice, read in the page, with its PDF to keep. */
76
- function Bill({ invoice, onClose }) {
69
+ /** One invoice, read in the page, with its PDF to keep. Drawn while it is open, so each opens anew. */
70
+ function Bill({ invoice: b, onClose }) {
77
71
  const t = useTarget();
78
72
  const [note, setNote] = useState('');
79
73
  const [working, setWorking] = useState(false);
80
- const b = invoice;
81
74
  const keep = async () => {
82
- if (!b)
83
- return;
84
75
  setWorking(true);
85
76
  setNote('');
86
77
  try {
@@ -93,12 +84,12 @@ function Bill({ invoice, onClose }) {
93
84
  window.setTimeout(() => URL.revokeObjectURL(url), 1000);
94
85
  }
95
86
  catch (e) {
96
- setNote(e instanceof Error ? e.message : 'The PDF did not download');
87
+ setNote(e.message);
97
88
  }
98
89
  finally {
99
90
  setWorking(false);
100
91
  }
101
92
  };
102
- const line = (what, cents, strong = false) => (_jsxs(XStack, { justify: "space-between", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: what }), _jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: b ? money(cents, b.currency) : '' })] }, what));
103
- return (_jsx(Dialog, { open: Boolean(b), onOpenChange: (o) => (o ? undefined : onClose()), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: b?.number ? `Invoice ${b.number}` : 'Invoice' }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: onClose, children: _jsx(X, { size: 16 }) })] }), b ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: [day(b.date), word(b.status), b.start && b.end ? `for ${day(b.start)} – ${day(b.end)}` : ''].filter(Boolean).join(' · ') }), _jsx(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: b.lines.length ? b.lines.map((l, i) => line(l.description || `Line ${i + 1}`, l.amount)) : line('Subtotal', b.subtotal) }), _jsxs(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: [b.discount ? line('Discount', -b.discount) : null, b.tax ? line('Tax', b.tax) : null, line('Total', b.total, true), b.credit ? line('Credit applied', -b.credit) : null, line('Paid', b.paid), b.due ? line('Due', b.due, true) : null] }), _jsx(Note, { children: note }), _jsx(XStack, { justify: "flex-end", children: _jsxs(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void keep(), children: [_jsx(Download, { size: 14 }), " Download PDF"] }) })] })) : null] }) }));
93
+ const line = (what, cents, strong = false) => (_jsxs(XStack, { justify: "space-between", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: what }), _jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: money(cents, b.currency) })] }, what));
94
+ return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: b.number ? `Invoice ${b.number}` : 'Invoice' }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: onClose, children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: [day(b.date), word(b.status), b.start && b.end ? `for ${day(b.start)} – ${day(b.end)}` : ''].filter(Boolean).join(' · ') }), _jsx(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: b.lines.length ? b.lines.map((l, i) => line(l.description || `Line ${i + 1}`, l.amount)) : line('Subtotal', b.subtotal) }), _jsxs(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: [b.discount ? line('Discount', -b.discount) : null, b.tax ? line('Tax', b.tax) : null, line('Total', b.total, true), b.credit ? line('Credit applied', -b.credit) : null, line('Paid', b.paid), b.due ? line('Due', b.due, true) : null] }), _jsx(Note, { children: note }), _jsx(XStack, { justify: "flex-end", children: _jsxs(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void keep(), children: [_jsx(Download, { size: 14 }), " Download PDF"] }) })] })] }) }));
104
95
  }
@@ -40,7 +40,7 @@ export function Capabilities() {
40
40
  list.reload();
41
41
  }
42
42
  catch (e) {
43
- setNote(e instanceof Error ? e.message : 'That was not saved');
43
+ setNote(e.message);
44
44
  }
45
45
  finally {
46
46
  setBusy('');
@@ -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
  }