@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
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
  }
package/lib/run.js CHANGED
@@ -15,23 +15,36 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  * A sandbox run is one agent invocation that takes its task on its command line
16
16
  * (apps/coding steer.go), so what continues it is a new run: a paused run is
17
17
  * carried on, and a finished one followed up, by `POST /v1/agent/coding` from
18
- * the branch its work was kept on — the platform's own continue, done here.
18
+ * the branch its work was kept on — the platform's own continue, done here —
19
+ * with the model and effort the composer's foot shows, which New shares.
20
+ *
21
+ * It reads as a chat: the person's ask opens it, each thing the agent said can
22
+ * be copied and judged (a verdict is `build.verdict` on the event bus, as a
23
+ * project's turns send it), and the title is the menu of what can be done to
24
+ * the run. Share opens its story to the public build route.
19
25
  */
20
26
  import { SizableText, XStack, YStack } from '@hanzo/gui';
21
- import { Copy, ExternalLink, GitPullRequest, Globe, MoreHorizontal, PanelRight, Pencil } from '@hanzogui/lucide-icons-2';
27
+ import { ChevronDown, ChevronRight, Copy, ExternalLink, GitPullRequest, Globe, PanelRight, Pencil, Share } from '@hanzogui/lucide-icons-2';
22
28
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
23
29
  import { Steer } from '@hanzo/ui/agents';
24
30
  import { Composer } from '@hanzo/ui/chat';
31
+ import { ChipSelect } from '@hanzo/ui/product';
25
32
  import { useEffect, useMemo, useState } from 'react';
26
33
  import { approve, followUp, start } from './api/coding.js';
34
+ import { ENSO, label as named, models } from './api/models.js';
27
35
  import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
28
36
  import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
37
+ import { verdict } from './api/verdict.js';
29
38
  import { useKept, useRead, useRun } from './data.js';
30
39
  import { Desk } from './desk.js';
31
40
  import { useHost, useTarget } from './host.js';
41
+ import { EFFORTS } from './landing.js';
32
42
  import { Out } from './out.js';
43
+ import { usePrefs } from './prefs.js';
33
44
  import { Transcript } from './transcript.js';
34
45
  const LIVE = new Set(['running', 'paused', '']);
46
+ // What this browser kept is read as it is found: a stored value that is not a string is none.
47
+ const text = (v) => (typeof v === 'string' ? v : '');
35
48
  export function Run({ id }) {
36
49
  const host = useHost();
37
50
  const t = useTarget();
@@ -48,6 +61,13 @@ export function Run({ id }) {
48
61
  const [naming, setNaming] = useState(false);
49
62
  const [name, setName] = useState('');
50
63
  const [copied, setCopied] = useState('');
64
+ const [unfolded, setUnfolded] = useState(false);
65
+ // The model and effort a run started here uses: New's choice, which a change here changes too.
66
+ const { prefs } = usePrefs();
67
+ const [chose, choose] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
68
+ const model = text(chose?.model) || prefs.code?.model || ENSO;
69
+ const pace = EFFORTS.find((e) => e.id === chose?.effort) ?? EFFORTS.find((e) => e.id === prefs.code?.effort) ?? EFFORTS[1];
70
+ const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
51
71
  const mode = record?.mode ?? '';
52
72
  const shown = useMemo(() => cards(events, mode), [events, mode]);
53
73
  const end = outcome(events);
@@ -62,9 +82,11 @@ export function Run({ id }) {
62
82
  // A sandbox run is held by the coding service, which pauses it with its work
63
83
  // kept and has no resume of its own; a machine drains its commands itself.
64
84
  const held = (record?.environment || 'sandbox') === 'sandbox';
85
+ // Words go to the run while it works, unless it is a paused sandbox run, which a new run carries on.
86
+ const steering = running && !(paused && held);
65
87
  // A follow-up clones what the run kept, so it waits until the run has said where that is.
66
88
  const waiting = (paused && held && !kept.settled) || (finished && stopping && !kept.settled);
67
- const title = record?.title || detail.value?.title || (detail.loading ? '' : 'Untitled run');
89
+ const title = record?.title || (detail.loading ? '' : 'Untitled run');
68
90
  // A setup run explores and installs before it answers, which takes minutes.
69
91
  const setup = mode === 'setup';
70
92
  const [seen, setSeen] = useKept('hanzo.build.setup.seen', false);
@@ -86,15 +108,17 @@ export function Run({ id }) {
86
108
  const planned = mode === 'plan' && finished && state === 'done' ? answer(events) : '';
87
109
  // What the recorded read left out: it carries the latest fifty turns.
88
110
  const hidden = Math.max(0, (detail.value?.events ?? 0) - (detail.value?.recent.length ?? 0));
111
+ // Only a run still working is on a step.
112
+ const current = running ? plan.find((x) => !x.done) : undefined;
113
+ // Asked for while it ran, said once when it stops. `notify` is only ever set
114
+ // where Notification exists; the permission can be taken back since.
89
115
  useEffect(() => {
90
116
  if (!notify || running)
91
117
  return;
92
- if (typeof Notification === 'undefined' || Notification.permission !== 'granted')
93
- return;
94
- const n = new Notification(title || 'Run finished', { body: 'This run has finished.' });
118
+ if (Notification.permission === 'granted')
119
+ new Notification(record?.title || 'Run finished', { body: 'This run has finished.' });
95
120
  setNotify(false);
96
- return () => n.close();
97
- }, [notify, running, title]);
121
+ }, [notify, running, record?.title]);
98
122
  const ask = async () => {
99
123
  if (typeof Notification === 'undefined') {
100
124
  setNote('This browser cannot send a notification.');
@@ -108,8 +132,8 @@ export function Run({ id }) {
108
132
  setNotify(true);
109
133
  setNote('');
110
134
  };
111
- /** Something that answers a sentence for the note, or throws the reason. */
112
- const act = async (work, failed) => {
135
+ /** Something that answers a sentence for the note, or throws the reason. One at a time. */
136
+ const act = async (work) => {
113
137
  if (busy)
114
138
  return;
115
139
  setBusy(true);
@@ -118,36 +142,37 @@ export function Run({ id }) {
118
142
  setNote(await work());
119
143
  }
120
144
  catch (e) {
121
- setNote(e instanceof Error ? e.message : failed);
145
+ setNote(e.message);
122
146
  }
123
147
  finally {
124
148
  setBusy(false);
125
149
  }
126
150
  };
151
+ /** A new run's ask with the model and effort the foot shows. The router is the platform's default, so it is not named. */
152
+ const tuned = (a) => ({ ...a, model: model === ENSO ? undefined : model, effort: pace.id });
127
153
  /** A new run from this one's work, opened. A paused run it carries on is let go once the new one is admitted. */
128
154
  const carry = async (said, how = 'follow') => {
129
155
  if (!earlier)
130
156
  throw new Error('This run is still being read');
131
- const run = await start(t, how === 'approve' ? approve(earlier, said) : followUp(earlier, said));
157
+ const run = await start(t, tuned(how === 'approve' ? approve(earlier, said) : followUp(earlier, said)));
132
158
  if (paused)
133
159
  await stop(t, id, 'Continued in a follow-up run').catch(() => undefined);
134
160
  setDraft('');
135
161
  host.go(run.session);
136
162
  return '';
137
163
  };
164
+ // The composer sends only words, and only while it is enabled.
138
165
  const send = () => {
139
166
  const words = draft.trim();
140
- if (!words)
141
- return;
142
- if (running && !(paused && held)) {
167
+ if (steering) {
143
168
  void act(async () => {
144
169
  await message(t, id, words);
145
170
  setDraft('');
146
171
  return 'Sent — recorded on this run';
147
- }, 'Could not reach this run');
172
+ });
148
173
  return;
149
174
  }
150
- void act(() => carry(words), 'The follow-up could not start');
175
+ void act(() => carry(words));
151
176
  };
152
177
  const command = (c) => {
153
178
  switch (c) {
@@ -156,22 +181,22 @@ export function Run({ id }) {
156
181
  await stop(t, id);
157
182
  setStopping(true);
158
183
  return 'Stop requested — the run keeps its work on its branch';
159
- }, 'Could not stop this run');
184
+ });
160
185
  return;
161
186
  case 'pause':
162
187
  void act(async () => {
163
188
  await pause(t, id);
164
189
  return held ? 'Pause requested — the run keeps its work on its branch and waits' : 'Pause asked — the machine pauses when it reads it';
165
- }, 'Could not pause this run');
190
+ });
166
191
  return;
167
192
  case 'resume':
168
193
  if (held)
169
- void act(() => carry(''), 'The run could not go on');
194
+ void act(() => carry(''));
170
195
  else
171
196
  void act(async () => {
172
197
  await resume(t, id);
173
198
  return 'Resume asked — the machine goes on when it reads it';
174
- }, 'Could not resume this run');
199
+ });
175
200
  return;
176
201
  }
177
202
  };
@@ -188,9 +213,9 @@ export function Run({ id }) {
188
213
  await publish(t, id, on);
189
214
  detail.reload();
190
215
  return on ? 'Shared — anyone with the link can read this run’s story' : 'No longer shared';
191
- }, 'Could not change who can read this run');
192
- const link = record ? story(t, record.org || host.org || '', record.project) : '';
193
- const menu = [
216
+ });
217
+ // What the title's menu does to the run.
218
+ const manage = [
194
219
  ...(signed && record
195
220
  ? [
196
221
  {
@@ -204,25 +229,24 @@ export function Run({ id }) {
204
229
  },
205
230
  ]
206
231
  : []),
207
- // Sharing opens the public build route, which is addressed by the project a run built.
208
- ...(signed && record?.project
209
- ? [
210
- record.published
211
- ? { key: 'unshare', label: 'Stop sharing', icon: _jsx(Globe, { size: 16 }), onSelect: () => void share(false) }
212
- : { key: 'share', label: 'Share publicly', icon: _jsx(Globe, { size: 16 }), description: 'Anyone with the link can read it', onSelect: () => void share(true) },
213
- ]
214
- : []),
215
- ...(record?.published && link
216
- ? [{ key: 'link', label: copied === 'link' ? 'Copied' : 'Copy public link', icon: _jsx(Copy, { size: 16 }), description: link, onSelect: () => void copy('link', link) }]
217
- : []),
218
232
  { key: 'copy', label: copied === 'id' ? 'Copied' : 'Copy run id', icon: _jsx(Copy, { size: 16 }), description: id, onSelect: () => void copy('id', id) },
219
233
  ];
234
+ // Sharing opens the public build route, which is addressed by the org and the project a run built.
235
+ const link = record?.published ? story(t, record.org || host.org, record.project) : '';
236
+ const sharing = signed && record?.project
237
+ ? [
238
+ record.published
239
+ ? { key: 'unshare', label: 'Stop sharing', icon: _jsx(Globe, { size: 16 }), onSelect: () => void share(false) }
240
+ : { key: 'share', label: 'Share publicly', icon: _jsx(Globe, { size: 16 }), description: 'Anyone with the link can read it', onSelect: () => void share(true) },
241
+ ...(link ? [{ key: 'link', label: copied === 'link' ? 'Copied' : 'Copy public link', icon: _jsx(Copy, { size: 16 }), description: link, onSelect: () => void copy('link', link) }] : []),
242
+ ]
243
+ : [];
220
244
  const save = () => void act(async () => {
221
245
  await rename(t, id, name);
222
246
  setNaming(false);
223
247
  detail.reload();
224
248
  return 'Renamed';
225
- }, 'Could not rename this run');
249
+ });
226
250
  const placeholder = !signed
227
251
  ? 'Sign in to follow up'
228
252
  : waiting
@@ -238,9 +262,11 @@ export function Run({ id }) {
238
262
  : finished
239
263
  ? 'Follow up — continues in a new run'
240
264
  : 'Reading this run…';
241
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, menu.length ? (_jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Manage this run", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: menu })) : null] }), _jsx(Transcript, { cards: shown,
265
+ // What a new run from here is made with; words that steer this run use neither.
266
+ const tuning = signed && !steering ? (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", justify: "flex-end", flexWrap: "wrap", rowGap: "$1", children: [_jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === model)?.label ?? named(model), chosen: catalog.value.find((m) => m.id === model) ?? null, items: catalog.value, onChange: (m) => choose({ ...chose, model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: pace.label, chosen: pace, items: EFFORTS, onChange: (e) => choose({ ...chose, effort: e.id }), placement: "top-end", width: 180 })] })) : undefined;
267
+ return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
242
268
  // A paused run's agent was stopped where it stood; what it was running is cut off.
243
- live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve'), 'The build could not start') : undefined, approving: busy, empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
269
+ live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve')) : undefined, approving: busy, onVerdict: (v) => verdict(t, id, record?.project ?? '', v), empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
244
270
  ? detail.error.message
245
271
  : detail.loading
246
272
  ? 'Reading this run…'
@@ -248,11 +274,12 @@ export function Run({ id }) {
248
274
  ? setup
249
275
  ? 'Setting up environment'
250
276
  : 'Starting…'
251
- : 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$4", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || (signed && refused) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Steps" }), _jsx(SizableText, { size: "$1", color: "$soft", children: String(plan.length) })] }), plan.map((s, i) => {
252
- // Only a run still working is on a step.
253
- const current = running && !s.done && plan.findIndex((x) => !x.done) === i;
254
- return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || current ? '$ink' : '$soft', children: s.done ? '✓' : current ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
255
- })] })) : null, setup && running && !seen ? _jsx(Onboarding, { onDone: () => setSeen(true) }) : null, running ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 160, children: paused
277
+ : 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$3", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || (signed && refused) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-label": `Steps · ${plan.length}`, "aria-expanded": unfolded, onPress: () => setUnfolded(!unfolded), px: "$3", py: "$1.5", gap: "$2", items: "center", hoverStyle: { bg: '$hover' }, children: [unfolded ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: `Steps · ${plan.length}` }), current && !unfolded ? (_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: { textAlign: 'right' }, children: current.name })) : null] }), unfolded
278
+ ? plan.map((s) => {
279
+ const on = s === current;
280
+ return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || on ? '$ink' : '$soft', children: s.done ? '✓' : on ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
281
+ })
282
+ : null] })) : null, setup && running && !seen ? _jsx(Onboarding, { onDone: () => setSeen(true) }) : null, running ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 160, children: paused
256
283
  ? kept.pushed
257
284
  ? 'Paused — its work so far is on its branch.'
258
285
  : 'Paused.'
@@ -263,8 +290,8 @@ export function Run({ id }) {
263
290
  : 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
264
291
  ...(paused ? ['pause'] : ['resume']),
265
292
  ...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
266
- ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: running && !(paused && held) ? 'Steer this run' : 'Follow up on this run' })] })] }), desk ? (_jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', events: events, live: running, menu: menu, refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
267
- if ((e.key ?? e.nativeEvent?.key) === 'Enter')
293
+ ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
294
+ if (e.key === 'Enter')
268
295
  save();
269
296
  } }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
270
297
  }
@@ -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('');