@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/customize/ui.js
CHANGED
|
@@ -9,6 +9,7 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
|
9
9
|
import { Check, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
10
10
|
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
11
11
|
import { useState } from 'react';
|
|
12
|
+
import { useHost } from '../host.js';
|
|
12
13
|
export const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
13
14
|
/** A button that holds a card's text reads from the left, not from the middle a button centres it in. */
|
|
14
15
|
export const left = { textAlign: 'left' };
|
|
@@ -40,7 +41,7 @@ export function Tile({ title, detail, meta, mark, action, onOpen, }) {
|
|
|
40
41
|
}
|
|
41
42
|
/** The one card the shelf puts first, drawn larger. */
|
|
42
43
|
export function Featured({ title, detail, meta, mark, action, onOpen }) {
|
|
43
|
-
return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", style: left, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: "Featured" }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }),
|
|
44
|
+
return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", style: left, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: "Featured" }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta })] })] }), action] }));
|
|
44
45
|
}
|
|
45
46
|
/** A card's add control: a plus while it is not yours, a check once it is. */
|
|
46
47
|
export function Add({ name, added, busy, onPress }) {
|
|
@@ -65,7 +66,7 @@ export function Confirm({ what, says, open, onOpenChange, onYes }) {
|
|
|
65
66
|
onOpenChange(false);
|
|
66
67
|
}
|
|
67
68
|
catch (e) {
|
|
68
|
-
setNote(e
|
|
69
|
+
setNote(e.message);
|
|
69
70
|
}
|
|
70
71
|
finally {
|
|
71
72
|
setBusy(false);
|
|
@@ -77,6 +78,11 @@ export function Confirm({ what, says, open, onOpenChange, onYes }) {
|
|
|
77
78
|
export function Soft({ children, action }) {
|
|
78
79
|
return (_jsxs(YStack, { py: "$6", items: "center", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }), action] }));
|
|
79
80
|
}
|
|
81
|
+
/** What a signed-out visitor is told, with the one control that signs them in. */
|
|
82
|
+
export function Visitor({ children }) {
|
|
83
|
+
const host = useHost();
|
|
84
|
+
return (_jsx(Soft, { action: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }), children: children }));
|
|
85
|
+
}
|
|
80
86
|
/** What the last action did, said once, where it was done. */
|
|
81
87
|
export function Line({ children }) {
|
|
82
88
|
if (!children)
|
|
@@ -85,7 +91,7 @@ export function Line({ children }) {
|
|
|
85
91
|
}
|
|
86
92
|
/** A small heading over a run of cards. */
|
|
87
93
|
export function Part({ title, detail, children }) {
|
|
88
|
-
return (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }),
|
|
94
|
+
return (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: "$soft", children: detail })] }), children] }));
|
|
89
95
|
}
|
|
90
96
|
/** A labelled control, with a line under it when there is more to say. */
|
|
91
97
|
export function Field({ label, hint, children }) {
|
package/lib/data.js
CHANGED
|
@@ -32,7 +32,8 @@ export function useRead(load, initial, key) {
|
|
|
32
32
|
setValue(v);
|
|
33
33
|
setError(null);
|
|
34
34
|
})
|
|
35
|
-
|
|
35
|
+
// Every read goes through the platform's client, which refuses with an Error.
|
|
36
|
+
.catch((e) => live && setError(e))
|
|
36
37
|
.finally(() => live && setLoading(false));
|
|
37
38
|
return () => {
|
|
38
39
|
live = false;
|
|
@@ -50,27 +51,28 @@ export function useRecents(t, signed) {
|
|
|
50
51
|
const read = useRead(signed ? () => list(t, { kind: 'coding', limit: 50 }) : null, [], [t, signed]);
|
|
51
52
|
const [rows, setRows] = useState([]);
|
|
52
53
|
useEffect(() => setRows(read.value), [read.value]);
|
|
53
|
-
// A feed of the whole org carries every agent's heartbeat, so an unseen id
|
|
54
|
-
// re-reads the list at most every five seconds.
|
|
55
54
|
const last = useRef(0);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const now = Date.now();
|
|
59
|
-
if (now - last.current < 5000)
|
|
60
|
-
return;
|
|
61
|
-
last.current = now;
|
|
62
|
-
read.reload();
|
|
63
|
-
};
|
|
55
|
+
// useRead's reload is the same function every render, so it never reopens the feed.
|
|
56
|
+
const again = read.reload;
|
|
64
57
|
useEffect(() => {
|
|
65
58
|
if (!signed)
|
|
66
59
|
return;
|
|
60
|
+
// A feed of the whole org carries every agent's heartbeat, so an unseen id
|
|
61
|
+
// re-reads the list at most every five seconds.
|
|
62
|
+
const reload = () => {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
if (now - last.current < 5000)
|
|
65
|
+
return;
|
|
66
|
+
last.current = now;
|
|
67
|
+
again();
|
|
68
|
+
};
|
|
67
69
|
const ctl = new AbortController();
|
|
68
70
|
void watch(t, '', {
|
|
69
71
|
session: (s) => setRows((prev) => {
|
|
70
72
|
const i = prev.findIndex((r) => r.id === s.id);
|
|
71
73
|
if (i === -1) {
|
|
72
74
|
// A run this list has not seen: re-read rather than guess its kind.
|
|
73
|
-
reload
|
|
75
|
+
reload();
|
|
74
76
|
return prev;
|
|
75
77
|
}
|
|
76
78
|
const next = prev.slice();
|
|
@@ -79,13 +81,13 @@ export function useRecents(t, signed) {
|
|
|
79
81
|
}),
|
|
80
82
|
open: (n) => {
|
|
81
83
|
if (n > 0)
|
|
82
|
-
reload
|
|
84
|
+
reload();
|
|
83
85
|
},
|
|
84
86
|
}, ctl.signal).catch(() => {
|
|
85
87
|
/* a refused feed leaves the list as read */
|
|
86
88
|
});
|
|
87
89
|
return () => ctl.abort();
|
|
88
|
-
}, [t, signed]);
|
|
90
|
+
}, [t, signed, again]);
|
|
89
91
|
return { ...read, value: rows };
|
|
90
92
|
}
|
|
91
93
|
export function usePlaces(t, signed) {
|
|
@@ -149,7 +151,7 @@ export function useRun(t, id) {
|
|
|
149
151
|
if (n > 0)
|
|
150
152
|
reload.current();
|
|
151
153
|
},
|
|
152
|
-
}, ctl.signal).catch((e) => setRefused(e
|
|
154
|
+
}, ctl.signal).catch((e) => setRefused(e.message));
|
|
153
155
|
return () => ctl.abort();
|
|
154
156
|
}, [t, id]);
|
|
155
157
|
// The read and the frames describe THIS run or nothing: a read kept from the
|
package/lib/desk.d.ts
CHANGED
|
@@ -17,11 +17,11 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ti
|
|
|
17
17
|
sandbox: string;
|
|
18
18
|
events: Event[];
|
|
19
19
|
live: boolean;
|
|
20
|
-
/** What can be done to the run — rename, share, copy its id — as its header
|
|
21
|
-
menu
|
|
20
|
+
/** What can be done to the run — rename, share, copy its id — as its header offers it. */
|
|
21
|
+
menu: NonNullable<DropdownMenuProps['items']>;
|
|
22
22
|
refused: string;
|
|
23
23
|
/** The refusal's one action: Retry, or Sign in when nobody is. */
|
|
24
|
-
retry
|
|
24
|
+
retry: string;
|
|
25
25
|
onRetry: () => void;
|
|
26
|
-
onHide
|
|
26
|
+
onHide: () => void;
|
|
27
27
|
}): import("react").JSX.Element;
|
package/lib/desk.js
CHANGED
|
@@ -14,13 +14,15 @@ import { Files } from './files.js';
|
|
|
14
14
|
import { Git } from './git.js';
|
|
15
15
|
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
16
16
|
const BLEED = ['desktop', 'terminal'];
|
|
17
|
-
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }) {
|
|
18
|
+
// Until a tab is picked it follows the run's mode, which is known once the run
|
|
19
|
+
// is read: a setup run's work IS the environment, so it opens there.
|
|
20
|
+
const [picked, setTab] = useState(null);
|
|
21
|
+
const tab = picked ?? (mode === 'setup' ? 'environment' : 'terminal');
|
|
20
22
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
21
23
|
const lines = shell(events);
|
|
22
24
|
const bleed = BLEED.includes(tab);
|
|
23
|
-
return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "subscriptions", tab: tab, onPick: setTab, children: "Subscriptions" })] }),
|
|
25
|
+
return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "subscriptions", tab: tab, onPick: setTab, children: "Subscriptions" })] }), _jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, live: live, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'files' ? (_jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: live, mode: mode, project: project, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'subscriptions' ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", px: "$6", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 320 }, children: "Ask an agent to subscribe to a Slack channel or thread, a pull request, or a timer." }) })) : null] })] }));
|
|
24
26
|
}
|
|
25
27
|
function TabButton({ id, tab, onPick, children }) {
|
|
26
28
|
const on = tab === id;
|
package/lib/door.js
CHANGED
package/lib/environment.js
CHANGED
|
@@ -63,7 +63,7 @@ export function Environment({ repo, busy }) {
|
|
|
63
63
|
return true;
|
|
64
64
|
}
|
|
65
65
|
catch (e) {
|
|
66
|
-
setNote(e
|
|
66
|
+
setNote(e.message);
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
69
|
finally {
|
|
@@ -78,7 +78,7 @@ export function Environment({ repo, busy }) {
|
|
|
78
78
|
host.go(run.session);
|
|
79
79
|
}
|
|
80
80
|
catch (e) {
|
|
81
|
-
setNote(e
|
|
81
|
+
setNote(e.message);
|
|
82
82
|
setWorking(false);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
@@ -116,7 +116,7 @@ export function SetupDialog({ repo, open, onOpenChange, onStart, onSaved, }) {
|
|
|
116
116
|
done();
|
|
117
117
|
}
|
|
118
118
|
catch (e) {
|
|
119
|
-
setNote(e
|
|
119
|
+
setNote(e.message);
|
|
120
120
|
}
|
|
121
121
|
finally {
|
|
122
122
|
setWorking(false);
|
package/lib/find.js
CHANGED
|
@@ -44,7 +44,7 @@ export function Find({ open, onOpenChange, recents, onOpen, }) {
|
|
|
44
44
|
})
|
|
45
45
|
.catch((e) => {
|
|
46
46
|
if (mine === asked.current)
|
|
47
|
-
setError(e
|
|
47
|
+
setError(e.message);
|
|
48
48
|
})
|
|
49
49
|
.finally(() => {
|
|
50
50
|
if (mine === asked.current)
|
|
@@ -71,7 +71,7 @@ export function Find({ open, onOpenChange, recents, onOpen, }) {
|
|
|
71
71
|
return n ? all.filter((h) => `${h.title} ${h.line}`.toLowerCase().includes(n)) : all;
|
|
72
72
|
}, [q, rows, recents, status]);
|
|
73
73
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, children: [_jsx(DialogTitle, { children: "Find a run" }), _jsx(Input, { autoFocus: true, value: q, onChangeText: setQ, placeholder: "Search runs\u2026", "aria-label": "Search runs", onKeyDown: (e) => {
|
|
74
|
-
if (
|
|
74
|
+
if (e.key === 'Enter' && hits[0])
|
|
75
75
|
onOpen(hits[0].id);
|
|
76
76
|
} }), _jsx(XStack, { gap: "$1", flexWrap: "wrap", role: "group", "aria-label": "Status", children: FILTERS.map((f) => (_jsx(XStack, { render: "button", "aria-label": f.label, "aria-pressed": status === f.id, onPress: () => setStatus(f.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: status === f.id ? '$ink' : '$borderColor', bg: status === f.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$1", color: status === f.id ? '$ink' : '$soft', children: f.label }) }, f.id || 'all'))) }), _jsx(ScrollView, { maxH: 360, children: _jsxs(YStack, { gap: "$1", role: "list", children: [hits.map((h) => (_jsxs(XStack, { role: "listitem", render: "button", "aria-label": h.title, onPress: () => onOpen(h.id), px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, items: "center", gap: "$3", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: h.title }), h.line ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, shrink: 0, maxW: "45%", children: h.line })) : null] }, h.id))), !loading && !error && hits.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", children: rows && next ? 'No run read so far matches.' : 'No run matches.' })) : null] }) }), error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: error })) : null, loading ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: "Reading runs\u2026" })) : next ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => void read(next), children: "Older runs" }) })) : null] }) }));
|
|
77
77
|
}
|
package/lib/forge.js
CHANGED
|
@@ -69,7 +69,7 @@ function Automations() {
|
|
|
69
69
|
setTick((n) => n + 1);
|
|
70
70
|
}
|
|
71
71
|
catch (e) {
|
|
72
|
-
setProblem(e
|
|
72
|
+
setProblem(e.message);
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: "Automations" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Repeating work for this organization. A new one starts off, so you can name it before it runs." })] }), _jsxs(Button, { size: "sm", shrink: 0, disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New automation"] })] }), _jsx(XStack, { justify: "flex-end", children: _jsx(YStack, { width: 240, maxW: "100%", children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search\u2026", "aria-label": "Search automations", disabled: !signed }) }) }), problem ? _jsx(Note, { children: problem }) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Empty, { says: "Sign in to see this organization's automations." })) : list.loading && list.value.length === 0 ? (_jsx(Empty, { says: "Reading automations\u2026" })) : shown.length === 0 ? (_jsx(Empty, { says: list.value.length === 0 ? 'No automations yet.' : 'Nothing matches.', children: list.value.length === 0 ? (_jsx(Button, { size: "sm", onPress: () => setMaking(true), children: "New automation" })) : null })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Name" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 80, style: { textAlign: 'right' }, children: "Status" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: "Updated" })] }), shown.map((a) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: a.name }), _jsx(XStack, { render: "button", "aria-label": a.status === 'ENABLED' ? `Turn off ${a.name}` : `Turn on ${a.name}`, onPress: () => void flip(a), width: 80, justify: "flex-end", children: _jsx(SizableText, { size: "$1", color: "$ink", children: a.status === 'ENABLED' ? 'On' : 'Off' }) }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: a.updated ? when(new Date(a.updated).toISOString()) : '—' })] }, a.id)))] }))] }), _jsx(NewAutomation, { open: making, onOpenChange: setMaking, onCreate: async (name) => {
|
|
@@ -93,7 +93,7 @@ function NewAutomation({ open, onOpenChange, onCreate, }) {
|
|
|
93
93
|
setName('');
|
|
94
94
|
}
|
|
95
95
|
catch (e) {
|
|
96
|
-
setError(e
|
|
96
|
+
setError(e.message);
|
|
97
97
|
}
|
|
98
98
|
finally {
|
|
99
99
|
setBusy(false);
|
|
@@ -198,7 +198,7 @@ function NewRepo({ open, onOpenChange, onCreate, }) {
|
|
|
198
198
|
setDescription('');
|
|
199
199
|
}
|
|
200
200
|
catch (e) {
|
|
201
|
-
setError(e
|
|
201
|
+
setError(e.message);
|
|
202
202
|
}
|
|
203
203
|
finally {
|
|
204
204
|
setBusy(false);
|
|
@@ -240,20 +240,12 @@ function Issues() {
|
|
|
240
240
|
const signed = Boolean(host.person);
|
|
241
241
|
const [board, setBoard] = useKept(boardKey(host.org), '');
|
|
242
242
|
const list = useRead(signed ? () => issues(t, board) : null, [], [t, signed, board]);
|
|
243
|
+
// Another screen chose the board (the rail's Issues chooses every board) while this one is open.
|
|
243
244
|
useEffect(() => {
|
|
244
|
-
const on = () =>
|
|
245
|
-
try {
|
|
246
|
-
const raw = window.localStorage.getItem(boardKey(host.org));
|
|
247
|
-
const next = raw === null ? '' : JSON.parse(raw);
|
|
248
|
-
setBoard(typeof next === 'string' ? next : '');
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
setBoard('');
|
|
252
|
-
}
|
|
253
|
-
};
|
|
245
|
+
const on = (e) => setBoard(e.detail);
|
|
254
246
|
window.addEventListener('hanzo-board', on);
|
|
255
247
|
return () => window.removeEventListener('hanzo-board', on);
|
|
256
|
-
}, [
|
|
248
|
+
}, [setBoard]);
|
|
257
249
|
const open = (w) => {
|
|
258
250
|
const name = w.repo || w.project;
|
|
259
251
|
if (name) {
|
package/lib/git.js
CHANGED
|
@@ -39,7 +39,7 @@ export function Git({ session, title, live }) {
|
|
|
39
39
|
: 'Open'
|
|
40
40
|
: c.pull.state === 'merged'
|
|
41
41
|
? 'Merged'
|
|
42
|
-
: 'Closed' }), c.pull.url.startsWith('https://') ? (_jsx(Out, { href: c.pull.url, label: `Open pull request #${c.pull.number}`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Open on the forge" }) })) : null] }), c.pull.reviews.length ? (c.pull.reviews.map((r, i) => (_jsxs(YStack, { gap: "$1", pb: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children:
|
|
42
|
+
: 'Closed' }), c.pull.url.startsWith('https://') ? (_jsx(Out, { href: c.pull.url, label: `Open pull request #${c.pull.number}`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Open on the forge" }) })) : null] }), c.pull.reviews.length ? (c.pull.reviews.map((r, i) => (_jsxs(YStack, { gap: "$1", pb: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: [r.author || 'Someone', verdict(r.state), when(r.at)].filter(Boolean).join(' · ') }), r.body ? (_jsx(SizableText, { size: "$2", color: "$ink", children: r.body })) : null] }, i)))) : (_jsx(Soft, { children: "No reviews yet" }))] })) : (_jsx(Soft, { children: "No pull request yet. The run opens one when it pushes its changes." }))) : c.commits.length ? (_jsx(YStack, { children: c.commits.map((k) => (_jsxs(XStack, { gap: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: k.sha.slice(0, 7) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 2, children: k.message }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [k.author, when(k.date)].filter(Boolean).join(' · ') })] })] }, k.sha))) })) : (_jsx(Soft, { children: "No pushed commits" }))] }));
|
|
43
43
|
}
|
|
44
44
|
function File({ change: f, first }) {
|
|
45
45
|
const [open, setOpen] = useState(false);
|
package/lib/host.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface Person {
|
|
|
17
17
|
avatar: string;
|
|
18
18
|
}
|
|
19
19
|
export interface Host {
|
|
20
|
+
/** The product's name, drawn at the top left: `Hanzo Build` on hanzo.build, `Hanzo` on hanzo.ai. */
|
|
21
|
+
name: string;
|
|
20
22
|
/** The platform origin, no trailing slash. */
|
|
21
23
|
api: string;
|
|
22
24
|
/** The signed-in person's bearer, or null. Read at call time, so a refresh is picked up. */
|
package/lib/landing.js
CHANGED
|
@@ -41,6 +41,13 @@ export const EFFORTS = [
|
|
|
41
41
|
];
|
|
42
42
|
const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium', ask: '' };
|
|
43
43
|
const COLUMN = 768;
|
|
44
|
+
/**
|
|
45
|
+
* What was typed here and not sent, for as long as this document lives. Signing
|
|
46
|
+
* in swaps a visitor's page for the rail, which draws New again in another
|
|
47
|
+
* place; the popup keeps the document so the sentence can come along, and this
|
|
48
|
+
* is where it waits.
|
|
49
|
+
*/
|
|
50
|
+
let unsent = { draft: '', files: [] };
|
|
44
51
|
export function Landing({ onStarted }) {
|
|
45
52
|
const host = useHost();
|
|
46
53
|
const t = useTarget();
|
|
@@ -50,8 +57,11 @@ export function Landing({ onStarted }) {
|
|
|
50
57
|
// Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
|
|
51
58
|
const kept = stored ?? { ...FIRST, ...prefs.code };
|
|
52
59
|
const set = (patch) => keep({ ...kept, ...patch });
|
|
53
|
-
const [draft, setDraft] = useState(kept.ask ||
|
|
54
|
-
const [files, setFiles] = useState(
|
|
60
|
+
const [draft, setDraft] = useState(kept.ask || unsent.draft);
|
|
61
|
+
const [files, setFiles] = useState(unsent.files);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
unsent = { draft, files };
|
|
64
|
+
}, [draft, files]);
|
|
55
65
|
const [busy, setBusy] = useState(false);
|
|
56
66
|
const [note, setNote] = useState('');
|
|
57
67
|
const [adding, setAdding] = useState(null);
|
|
@@ -81,18 +91,16 @@ export function Landing({ onStarted }) {
|
|
|
81
91
|
known.current.set(r.name, r);
|
|
82
92
|
return { repos, next: null };
|
|
83
93
|
}, [t]);
|
|
94
|
+
// Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
|
|
95
|
+
const home = kept.repo?.default_branch || 'main';
|
|
84
96
|
const loadBranches = useCallback(async (q) => {
|
|
85
|
-
|
|
86
|
-
return { branches: [], next: null };
|
|
87
|
-
const found = await one(t, kept.repo.name);
|
|
97
|
+
const found = await one(t, codebase);
|
|
88
98
|
const needle = q.trim().toLowerCase();
|
|
89
|
-
const names = (found?.branches ?? [
|
|
99
|
+
const names = (found?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
|
|
90
100
|
return { branches: names.map((name) => ({ name })), next: null };
|
|
91
|
-
}, [t,
|
|
101
|
+
}, [t, codebase, home]);
|
|
102
|
+
// The composer sends only a draft with words in it, and not while a send is out.
|
|
92
103
|
const send = async () => {
|
|
93
|
-
const prompt = draft.trim();
|
|
94
|
-
if (!prompt || busy)
|
|
95
|
-
return;
|
|
96
104
|
if (!signed) {
|
|
97
105
|
host.signIn?.();
|
|
98
106
|
return;
|
|
@@ -105,11 +113,12 @@ export function Landing({ onStarted }) {
|
|
|
105
113
|
setNote('');
|
|
106
114
|
try {
|
|
107
115
|
const run = await start(t, {
|
|
108
|
-
prompt: compose(
|
|
116
|
+
prompt: compose(draft.trim(), files),
|
|
109
117
|
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// An empty base or place is not sent: the default branch, and the sandbox.
|
|
119
|
+
repo: kept.repo.name,
|
|
120
|
+
base: kept.branch,
|
|
121
|
+
targetId: place.id,
|
|
113
122
|
mode: kept.mode,
|
|
114
123
|
model: kept.model === ENSO ? undefined : kept.model,
|
|
115
124
|
effort: kept.effort,
|
|
@@ -119,7 +128,7 @@ export function Landing({ onStarted }) {
|
|
|
119
128
|
onStarted(run.session);
|
|
120
129
|
}
|
|
121
130
|
catch (e) {
|
|
122
|
-
setNote(e
|
|
131
|
+
setNote(e.message);
|
|
123
132
|
}
|
|
124
133
|
finally {
|
|
125
134
|
setBusy(false);
|
|
@@ -146,7 +155,7 @@ export function Landing({ onStarted }) {
|
|
|
146
155
|
place: p,
|
|
147
156
|
})), [places.value, setUp]);
|
|
148
157
|
const branch = kept.branch || kept.repo?.default_branch || 'main';
|
|
149
|
-
const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id)
|
|
158
|
+
const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
|
|
150
159
|
const row = known.current.get(r.name) ?? chosen(r);
|
|
151
160
|
set({ repo: row, branch: row.default_branch });
|
|
152
161
|
}, load: loadRepos, troubleshoot: {
|
package/lib/markdown.js
CHANGED
|
@@ -5,7 +5,8 @@ const RULE = /^\s{0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
|
5
5
|
const QUOTE = /^\s{0,3}>\s?(.*)$/;
|
|
6
6
|
const ROW = /^\s*\|(.*)\|\s*$/;
|
|
7
7
|
const DIVIDER = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
8
|
-
|
|
8
|
+
/** A table row's cells. Only a line ROW matched is one. */
|
|
9
|
+
const cells = (line) => ROW.exec(line)[1].split('|').map((c) => c.trim());
|
|
9
10
|
/** The blocks of a markdown text, in order. */
|
|
10
11
|
export function blocks(md) {
|
|
11
12
|
const lines = md.replace(/\r\n?/g, '\n').split('\n');
|
|
@@ -25,7 +26,7 @@ export function blocks(md) {
|
|
|
25
26
|
i++;
|
|
26
27
|
while (i < lines.length && !lines[i].trim().startsWith(fence[1]))
|
|
27
28
|
body.push(lines[i++]);
|
|
28
|
-
out.push({ kind: 'code', lang: fence[2]
|
|
29
|
+
out.push({ kind: 'code', lang: fence[2], text: body.join('\n') });
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
31
32
|
if (!line.trim()) {
|
|
@@ -78,7 +79,7 @@ export function blocks(md) {
|
|
|
78
79
|
continue;
|
|
79
80
|
}
|
|
80
81
|
// A line indented under an item continues it.
|
|
81
|
-
if (lines[j].trim() && /^\s{2,}/.test(lines[j])
|
|
82
|
+
if (lines[j].trim() && /^\s{2,}/.test(lines[j])) {
|
|
82
83
|
items[items.length - 1].text += ` ${lines[j].trim()}`;
|
|
83
84
|
j++;
|
|
84
85
|
continue;
|
|
@@ -113,7 +114,8 @@ export function spans(text) {
|
|
|
113
114
|
out.push({ kind: 'strong', text: (m[5] ?? m[6]) });
|
|
114
115
|
else if (m[7] !== undefined || m[8] !== undefined)
|
|
115
116
|
out.push({ kind: 'em', text: (m[7] ?? m[8]) });
|
|
116
|
-
|
|
117
|
+
// What is left is the bare address, the only other thing INLINE matches.
|
|
118
|
+
else
|
|
117
119
|
out.push({ kind: 'link', text: m[9], href: m[9] });
|
|
118
120
|
at = m.index + m[0].length;
|
|
119
121
|
}
|
package/lib/pitch.d.ts
ADDED
package/lib/pitch.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What a visitor reads under New on hanzo.build: the same agent on their own
|
|
4
|
+
* computer, one line to install, and what it brings to a run. Each card names
|
|
5
|
+
* something this page or the CLI does today, and nothing it does not.
|
|
6
|
+
*/
|
|
7
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
8
|
+
import { Check, Copy, GitPullRequest, LayoutTemplate, ListChecks, MonitorPlay, Plug, Server, Sparkles, Workflow } from '@hanzogui/lucide-icons-2';
|
|
9
|
+
import { Button } from '@hanzo/ui';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { DOCS } from './foot.js';
|
|
12
|
+
export const INSTALL = 'curl -fsSL hanzo.sh | sh';
|
|
13
|
+
const FEATURES = [
|
|
14
|
+
{ icon: _jsx(Sparkles, { size: 18 }), title: 'Skills', body: 'Teach it a workflow once. A skill loads when a task matches, or when you call it by name.' },
|
|
15
|
+
{ icon: _jsx(ListChecks, { size: 18 }), title: 'Plan first', body: 'Start a run in plan mode. Nothing is built until you approve the plan.' },
|
|
16
|
+
{ icon: _jsx(Plug, { size: 18 }), title: 'Connectors and plugins', body: 'MCP servers, skills and agents behind one install. Your org’s credentials reach a run through a relay, never the sandbox.' },
|
|
17
|
+
{ icon: _jsx(Server, { size: 18 }), title: 'Environments', body: 'Point it at a repository. An agent works out install and start, and every later run begins from that setup.' },
|
|
18
|
+
{ icon: _jsx(MonitorPlay, { size: 18 }), title: 'Desktop and terminal', body: 'Watch each run’s browser and shell live, beside the conversation.' },
|
|
19
|
+
{ icon: _jsx(GitPullRequest, { size: 18 }), title: 'Review', body: 'Every run ends as a diff on its own branch, with commits and a pull request to review.' },
|
|
20
|
+
{ icon: _jsx(LayoutTemplate, { size: 18 }), title: 'Templates to live', body: 'Start from a working app and publish it to a live address.' },
|
|
21
|
+
{ icon: _jsx(Workflow, { size: 18 }), title: 'Automations', body: 'Hand it repeating work, and it runs on your schedule.' },
|
|
22
|
+
];
|
|
23
|
+
/** One line to copy, with a button that copies it. */
|
|
24
|
+
function Line({ text }) {
|
|
25
|
+
const [copied, setCopied] = useState(false);
|
|
26
|
+
const copy = () => {
|
|
27
|
+
void navigator.clipboard.writeText(text).then(() => {
|
|
28
|
+
setCopied(true);
|
|
29
|
+
setTimeout(() => setCopied(false), 1500);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", height: 52, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", width: "100%", maxW: 560, children: [_jsx(SizableText, { size: "$3", color: "$soft", fontFamily: "$mono", "aria-hidden": true, children: "$" }), _jsx(SizableText, { flex: 1, size: "$3", color: "$ink", fontFamily: "$mono", numberOfLines: 1, children: text }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": copied ? 'Copied' : 'Copy the install command', onPress: copy, children: copied ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }) })] }));
|
|
33
|
+
}
|
|
34
|
+
function Card({ icon, title, body }) {
|
|
35
|
+
return (_jsxs(YStack, { gap: "$2", p: "$5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flex: 1, minW: 240, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [icon, _jsx(SizableText, { size: "$4", fontWeight: "600", color: "$ink", children: title })] }), _jsx(SizableText, { size: "$3", color: "$soft", children: body })] }));
|
|
36
|
+
}
|
|
37
|
+
export function Pitch({ onStart }) {
|
|
38
|
+
return (_jsxs(YStack, { items: "center", px: "$4", pb: "$12", gap: "$12", children: [_jsxs(YStack, { id: "features", width: "100%", maxW: 1080, items: "center", gap: "$5", pt: "$6", children: [_jsx(SizableText, { size: "$2", color: "$soft", fontFamily: "$mono", children: "Powered by Zen 6 \u00B7 routed by Enso" }), _jsx(SizableText, { role: "heading", "aria-level": 2, size: "$9", color: "$ink", style: { textAlign: 'center' }, children: "Hanzo Dev" }), _jsx(SizableText, { size: "$4", color: "$soft", maxW: 620, style: { textAlign: 'center' }, children: "The agent. It runs in your terminal, on any codebase, in any language. This page is that same agent in the browser." }), _jsx(Line, { text: INSTALL }), _jsx(SizableText, { size: "$2", color: "$soft", fontFamily: "$mono", children: "dev opens it. dev \"fix the failing test\" hands it a task." }), _jsx("a", { href: DOCS, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: "Read the docs" }) }) })] }), _jsxs(YStack, { width: "100%", maxW: 1080, gap: "$5", children: [_jsx(SizableText, { role: "heading", "aria-level": 2, size: "$7", color: "$ink", children: "Everything a run needs" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: FEATURES.map((f) => (_jsx(Card, { ...f }, f.title))) })] }), _jsxs(YStack, { width: "100%", maxW: 1080, items: "center", gap: "$4", py: "$8", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(SizableText, { role: "heading", "aria-level": 2, size: "$7", color: "$ink", style: { textAlign: 'center' }, children: "Say what you want built." }), _jsx(SizableText, { size: "$3", color: "$soft", style: { textAlign: 'center' }, px: "$4", children: "In the browser here, or in your terminal." }), _jsx(Button, { variant: "primary", size: "sm", onPress: onStart, children: "Start building" })] })] }));
|
|
39
|
+
}
|
package/lib/plans.js
CHANGED
|
@@ -37,6 +37,7 @@ export function Plans() {
|
|
|
37
37
|
const [working, setWorking] = useState(false);
|
|
38
38
|
const [note, setNote] = useState('');
|
|
39
39
|
const on = current(subs.value);
|
|
40
|
+
const who = host.org ?? 'This organization';
|
|
40
41
|
const card = chosen(cards.value);
|
|
41
42
|
const yearly = list.value.some((p) => p.yearly > 0);
|
|
42
43
|
const free = (p) => p.monthly === 0 && !p.sales;
|
|
@@ -51,7 +52,7 @@ export function Plans() {
|
|
|
51
52
|
subs.reload();
|
|
52
53
|
}
|
|
53
54
|
catch (e) {
|
|
54
|
-
setNote(e
|
|
55
|
+
setNote(e.message);
|
|
55
56
|
}
|
|
56
57
|
finally {
|
|
57
58
|
setWorking(false);
|
|
@@ -73,10 +74,10 @@ export function Plans() {
|
|
|
73
74
|
if (!settled)
|
|
74
75
|
return null;
|
|
75
76
|
if (free(p)) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
return _jsx(Line, { children: `You move to ${p.name} when ${
|
|
77
|
+
// A free plan that is not the org's means a paid one is.
|
|
78
|
+
const paid = on;
|
|
79
|
+
if (paid.ending)
|
|
80
|
+
return _jsx(Line, { children: `You move to ${p.name} when ${paid.name} ends on ${day(paid.ends)}.` });
|
|
80
81
|
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: `Switch to ${p.name}` }));
|
|
81
82
|
}
|
|
82
83
|
if (on && on.price > 0)
|
|
@@ -89,17 +90,17 @@ export function Plans() {
|
|
|
89
90
|
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 1100, mx: "auto", px: "$5", py: "$5", gap: "$5", children: [_jsxs(XStack, { render: "button", "aria-label": "Back to billing", items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Billing" })] }), _jsxs(XStack, { items: "flex-end", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 240, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", children: "Plans that grow with you" }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
|
|
90
91
|
? 'Sign in to see the plan you are on.'
|
|
91
92
|
: on
|
|
92
|
-
? `${
|
|
93
|
-
: `${
|
|
93
|
+
? `${who} is on ${on.name}.`
|
|
94
|
+
: `${who} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
|
|
94
95
|
const cost = price(p, term);
|
|
95
96
|
return (_jsxs(YStack, { "aria-label": `${p.name} plan`, flex: 1, minW: 240, maxW: 360, "$max-sm": { maxW: '100%' }, gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: mine(p) || p.popular ? '$ink' : '$borderColor', bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$5", color: "$ink", children: p.name }), mine(p) ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Current" })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Popular" })) : null] }), p.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: p.description })) : null, _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$7", color: "$ink", children: p.sales ? 'Custom' : cost.month === 0 ? 'Free' : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
|
|
96
97
|
? 'Talk to us'
|
|
97
98
|
: cost.month === 0
|
|
98
99
|
? 'Forever'
|
|
99
100
|
: `${p.perSeat ? 'per seat, ' : ''}a month${term === 'year' && p.yearly ? `, ${money(cost.now, p.currency)} billed yearly` : ''}` })] }), _jsx(XStack, { children: move(p) }), p.features.length ? (_jsx(YStack, { gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: p.features.map((f) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(YStack, { pt: 2, children: _jsx(Check, { size: 14 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$soft", children: f })] }, f))) })) : null] }, p.id));
|
|
100
|
-
}) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) =>
|
|
101
|
+
}) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => !o && setBuying(null), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
|
|
101
102
|
await subscribe(t, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
|
|
102
|
-
return `${
|
|
103
|
+
return `${who} is on ${buying.name}`;
|
|
103
104
|
}), children: "Upgrade" })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Switch to the free plan?" }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${on.name} stays on until ${day(on.ends) || 'the end of this period'}, and nothing is charged after that.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: `Keep ${on.name}` }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
|
|
104
105
|
await cancel(t, on.id);
|
|
105
106
|
return `${on.name} ends on ${day(on.ends)}`;
|