@hanzo/build 0.2.17 → 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.
- package/lib/ask.js +2 -2
- package/lib/builder.js +36 -4
- package/lib/choice.js +2 -2
- package/lib/data.js +17 -15
- package/lib/find.js +2 -2
- package/lib/forge.js +6 -14
- package/lib/host.d.ts +2 -0
- package/lib/landing.js +25 -16
- package/lib/markdown.js +6 -4
- package/lib/pitch.d.ts +4 -0
- package/lib/pitch.js +39 -0
- package/lib/plans.js +10 -9
- package/lib/project.js +20 -21
- package/lib/prose.js +1 -1
- package/lib/publish.js +9 -8
- package/lib/settings/account.js +7 -5
- package/lib/settings/billing.js +11 -20
- package/lib/settings/capabilities.js +1 -1
- package/lib/settings/card.js +2 -2
- package/lib/settings/code.js +6 -3
- package/lib/settings/general.js +6 -5
- package/lib/settings/index.js +2 -3
- package/lib/settings/integrations.js +2 -2
- package/lib/settings/keys.js +8 -9
- package/lib/settings/machines.js +5 -9
- package/lib/settings/members.js +3 -2
- package/lib/settings/memory.js +1 -1
- package/lib/settings/notifications.js +1 -1
- package/lib/settings/privacy.js +2 -2
- package/lib/settings/usage.js +8 -9
- package/lib/shelf.js +5 -9
- package/lib/sync.js +2 -5
- package/lib/tools.js +2 -3
- package/lib/visit.d.ts +31 -0
- package/lib/visit.js +53 -0
- package/lib/voice.js +3 -5
- 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
|
|
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
|
|
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
|
|
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'
|
|
152
|
-
//
|
|
153
|
-
const to = new URL(e.path,
|
|
154
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
180
|
-
base:
|
|
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
|
|
189
|
+
setNote(e.message);
|
|
192
190
|
}
|
|
193
191
|
finally {
|
|
194
192
|
setBusy(false);
|
|
195
193
|
}
|
|
196
194
|
};
|
|
197
|
-
|
|
198
|
-
|
|
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,
|
|
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
|
|
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
|
|
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]
|
|
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
|
|
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:
|
|
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
|
|
88
|
+
setError(e.message);
|
|
87
89
|
setPhase('declared');
|
|
88
90
|
}
|
|
89
91
|
};
|
|
90
|
-
|
|
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 ${
|
|
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/settings/account.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
97
|
+
setNote(e.message);
|
|
97
98
|
}
|
|
98
99
|
finally {
|
|
99
100
|
setBusy(false);
|
|
100
101
|
}
|
|
101
102
|
};
|
|
102
|
-
const
|
|
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 })] })] }) }),
|
|
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
|
}
|
package/lib/settings/billing.js
CHANGED
|
@@ -39,7 +39,7 @@ export function Billing() {
|
|
|
39
39
|
setNote(done);
|
|
40
40
|
}
|
|
41
41
|
catch (e) {
|
|
42
|
-
setNote(e
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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:
|
|
103
|
-
return (_jsx(Dialog, { open:
|
|
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
|
}
|
package/lib/settings/card.js
CHANGED
|
@@ -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 {
|
package/lib/settings/code.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 }) }));
|
package/lib/settings/general.js
CHANGED
|
@@ -34,11 +34,12 @@ export function General() {
|
|
|
34
34
|
}
|
|
35
35
|
const keep = (patch) => {
|
|
36
36
|
setNote('');
|
|
37
|
-
save(patch).catch((e) => setNote(e
|
|
37
|
+
save(patch).catch((e) => setNote(e.message));
|
|
38
38
|
};
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
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
|
|
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
|
}
|
package/lib/settings/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
56
|
+
setNote(e.message);
|
|
57
57
|
setWorking(false);
|
|
58
58
|
}
|
|
59
59
|
};
|
package/lib/settings/keys.js
CHANGED
|
@@ -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",
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/lib/settings/machines.js
CHANGED
|
@@ -50,7 +50,7 @@ export function Machines() {
|
|
|
50
50
|
list.reload();
|
|
51
51
|
}
|
|
52
52
|
catch (e) {
|
|
53
|
-
setNote(e
|
|
53
|
+
setNote(e.message);
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsx(Group, { title: "Linked", children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading machines\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { status: m.status }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
@@ -64,21 +64,17 @@ export function Machines() {
|
|
|
64
64
|
] }) }, m.id))) })) }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
|
|
65
65
|
setNote(`${m.label} is registered`);
|
|
66
66
|
list.reload();
|
|
67
|
-
} }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
} }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
|
|
68
|
+
// Each dialog acts only while it is open, which is while its machine is chosen.
|
|
69
|
+
run: async (label) => {
|
|
70
70
|
await change(t, renaming.id, { label });
|
|
71
71
|
setNote(`Renamed to ${label}`);
|
|
72
72
|
list.reload();
|
|
73
73
|
} }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: `Remove ${removing?.label ?? 'this machine'}?`, says: "Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.", act: "Remove", run: async () => {
|
|
74
|
-
if (!removing)
|
|
75
|
-
return;
|
|
76
74
|
await remove(t, removing.id);
|
|
77
75
|
setNote(`${removing.label} is removed`);
|
|
78
76
|
list.reload();
|
|
79
77
|
} }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
|
|
80
|
-
if (!keying)
|
|
81
|
-
return;
|
|
82
78
|
const k = await key(t, keying.id);
|
|
83
79
|
setMinted({ machine: keying, key: k });
|
|
84
80
|
} }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
|
|
@@ -110,7 +106,7 @@ function Adding({ open, onOpenChange, onAdded }) {
|
|
|
110
106
|
close();
|
|
111
107
|
}
|
|
112
108
|
catch (e) {
|
|
113
|
-
setNote(e
|
|
109
|
+
setNote(e.message);
|
|
114
110
|
setWorking(false);
|
|
115
111
|
}
|
|
116
112
|
};
|
package/lib/settings/members.js
CHANGED
|
@@ -38,7 +38,7 @@ export function Members() {
|
|
|
38
38
|
setNote(done);
|
|
39
39
|
}
|
|
40
40
|
catch (e) {
|
|
41
|
-
setNote(e
|
|
41
|
+
setNote(e.message);
|
|
42
42
|
}
|
|
43
43
|
finally {
|
|
44
44
|
setWorking(false);
|
|
@@ -46,8 +46,9 @@ export function Members() {
|
|
|
46
46
|
};
|
|
47
47
|
const send = () => {
|
|
48
48
|
const address = email.trim();
|
|
49
|
+
// Invite is offered only once something is typed.
|
|
49
50
|
if (!EMAIL.test(address)) {
|
|
50
|
-
setNote(`${address
|
|
51
|
+
setNote(`${address} is not an email address`);
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
void act(() => invite(t, org, address), `${address} is invited. IAM sends no email: share the code with them.`, () => {
|
package/lib/settings/memory.js
CHANGED
package/lib/settings/privacy.js
CHANGED
|
@@ -33,7 +33,7 @@ export function Privacy() {
|
|
|
33
33
|
setShown(await setConsent(t, change));
|
|
34
34
|
}
|
|
35
35
|
catch (e) {
|
|
36
|
-
setNote(e
|
|
36
|
+
setNote(e.message);
|
|
37
37
|
}
|
|
38
38
|
finally {
|
|
39
39
|
setBusy('');
|
|
@@ -48,7 +48,7 @@ export function Privacy() {
|
|
|
48
48
|
setNote(`${saved.name} is ${saved.visibility || 'saved'}`);
|
|
49
49
|
}
|
|
50
50
|
catch (e) {
|
|
51
|
-
setNote(e
|
|
51
|
+
setNote(e.message);
|
|
52
52
|
}
|
|
53
53
|
finally {
|
|
54
54
|
setBusy('');
|