@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.
- package/lib/api/codebases.d.ts +1 -1
- package/lib/api/codebases.js +3 -3
- package/lib/api/harness.d.ts +7 -0
- package/lib/api/harness.js +34 -6
- package/lib/api/sessions.d.ts +2 -2
- package/lib/api/sessions.js +1 -1
- package/lib/api/turn.js +37 -16
- package/lib/ask.js +2 -2
- package/lib/builder.js +36 -4
- package/lib/choice.js +2 -2
- package/lib/customize/agents.js +4 -4
- package/lib/customize/connectors.js +7 -8
- package/lib/customize/index.js +3 -2
- package/lib/customize/plugins.js +3 -3
- package/lib/customize/skills.js +4 -4
- package/lib/customize/ui.d.ts +6 -2
- package/lib/customize/ui.js +9 -3
- package/lib/data.js +17 -15
- package/lib/desk.d.ts +4 -4
- package/lib/desk.js +6 -4
- package/lib/door.js +1 -1
- package/lib/environment.js +3 -3
- package/lib/find.js +2 -2
- package/lib/forge.js +6 -14
- package/lib/git.js +1 -1
- 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/run.js +72 -45
- 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/transcript.d.ts +5 -2
- package/lib/transcript.js +21 -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/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,
|
|
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 ||
|
|
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 (
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
}
|
|
172
|
+
});
|
|
148
173
|
return;
|
|
149
174
|
}
|
|
150
|
-
void act(() => carry(words)
|
|
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
|
-
}
|
|
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
|
-
}
|
|
190
|
+
});
|
|
166
191
|
return;
|
|
167
192
|
case 'resume':
|
|
168
193
|
if (held)
|
|
169
|
-
void act(() => carry('')
|
|
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
|
-
}
|
|
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
|
-
}
|
|
192
|
-
|
|
193
|
-
const
|
|
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
|
-
}
|
|
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
|
-
|
|
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')
|
|
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: "$
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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:
|
|
267
|
-
if (
|
|
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
|
}
|
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
|
}
|