@hanzo/build 0.2.17 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/ask.js +2 -2
- package/lib/builder.d.ts +10 -0
- package/lib/builder.js +59 -17
- package/lib/choice.js +2 -2
- package/lib/data.js +17 -15
- package/lib/find.js +2 -2
- package/lib/forge.js +6 -14
- package/lib/host.d.ts +2 -0
- package/lib/index.d.ts +8 -4
- package/lib/index.js +8 -4
- package/lib/landing.js +35 -21
- package/lib/markdown.js +6 -4
- package/lib/pitch.d.ts +4 -0
- package/lib/pitch.js +39 -0
- package/lib/plans.js +10 -9
- package/lib/project.js +20 -21
- package/lib/prose.js +1 -1
- package/lib/publish.js +9 -8
- package/lib/settings/account.js +7 -5
- package/lib/settings/billing.js +11 -20
- package/lib/settings/capabilities.js +1 -1
- package/lib/settings/card.js +2 -2
- package/lib/settings/code.js +6 -3
- package/lib/settings/general.js +6 -5
- package/lib/settings/index.js +2 -3
- package/lib/settings/integrations.js +2 -2
- package/lib/settings/keys.js +8 -9
- package/lib/settings/machines.js +5 -9
- package/lib/settings/members.js +3 -2
- package/lib/settings/memory.js +1 -1
- package/lib/settings/notifications.js +1 -1
- package/lib/settings/privacy.js +2 -2
- package/lib/settings/usage.js +8 -9
- package/lib/shelf.js +5 -9
- package/lib/sync.js +2 -5
- package/lib/tools.js +2 -3
- package/lib/visit.d.ts +31 -0
- package/lib/visit.js +53 -0
- package/lib/voice.js +3 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,7 +193,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
|
|
|
193
193
|
|
|
194
194
|
| call | what |
|
|
195
195
|
|---|---|
|
|
196
|
-
| `POST /v1/agent/coding` | start a run → 202 `{sessionId, …}` |
|
|
196
|
+
| `POST /v1/agent/coding` | start a run → 202 `{sessionId, …}`; with no `repo` the platform starts a new project named from the ask |
|
|
197
197
|
| `GET /v1/agent/sessions?kind=coding` | Recents, and a project's runs |
|
|
198
198
|
| `GET /v1/agent/sessions/{id}` · `GET /v1/agent/sessions/stream?root=` | a run, and its live feed (SSE over fetch) |
|
|
199
199
|
| `POST /v1/agent/sessions/{id}/message` · `/pause` · `/resume` · `/stop` | steer, pause, resume a machine's run, stop |
|
package/lib/ask.js
CHANGED
|
@@ -20,7 +20,7 @@ function useAct(onDone) {
|
|
|
20
20
|
onDone();
|
|
21
21
|
}
|
|
22
22
|
catch (e) {
|
|
23
|
-
setNote(e
|
|
23
|
+
setNote(e.message);
|
|
24
24
|
setWorking(false);
|
|
25
25
|
}
|
|
26
26
|
};
|
|
@@ -63,7 +63,7 @@ export function Rename({ open, onOpenChange, title, name, run, }) {
|
|
|
63
63
|
void go(() => run(next));
|
|
64
64
|
};
|
|
65
65
|
return (_jsx(Dialog, { open: open, onOpenChange: close, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: title }), _jsx(Input, { autoFocus: true, value: value, onChangeText: setValue, "aria-label": "Name", onKeyDown: (e) => {
|
|
66
|
-
if (
|
|
66
|
+
if (e.key === 'Enter')
|
|
67
67
|
save();
|
|
68
68
|
} }), _jsx(Said, { note: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => close(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: save, children: "Save" })] })] }) }));
|
|
69
69
|
}
|
package/lib/builder.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { type RailLink } from '@hanzo/ui/chat';
|
|
1
2
|
import { type Host } from './host.tsx';
|
|
3
|
+
/**
|
|
4
|
+
* The builder's places, as a rail draws them: four in view and the rest under
|
|
5
|
+
* More. Every one moves the builder through `go`, so a host that draws its own
|
|
6
|
+
* rail lists the same places, in the same words, as the builder's rail does.
|
|
7
|
+
*/
|
|
8
|
+
export declare function nav(host: Host, go: (path: string) => void): {
|
|
9
|
+
links: RailLink[];
|
|
10
|
+
more: RailLink[];
|
|
11
|
+
};
|
|
2
12
|
/**
|
|
3
13
|
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
|
4
14
|
*
|
package/lib/builder.js
CHANGED
|
@@ -24,15 +24,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
* bar and panes — and its mark leads back here.
|
|
25
25
|
*
|
|
26
26
|
* A host with a rail of its own mounts `<Builder rail={false}>` and draws the
|
|
27
|
-
* builder's places and runs in that rail
|
|
28
|
-
* left column, never two.
|
|
27
|
+
* builder's places (`nav`) and runs in that rail (`useSessions`, or `DevSection`,
|
|
28
|
+
* section.tsx): one left column, never two.
|
|
29
29
|
*/
|
|
30
30
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
31
31
|
import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu, Search, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
|
|
32
32
|
import { Button } from '@hanzo/ui';
|
|
33
33
|
import { SessionRail } from '@hanzo/ui/chat';
|
|
34
34
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
35
|
-
import { useMemo, useState } from 'react';
|
|
35
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
36
36
|
import { pinBoard } from './choice.js';
|
|
37
37
|
import { Customize } from './customize/index.js';
|
|
38
38
|
import { useKept, useRecents } from './data.js';
|
|
@@ -49,6 +49,8 @@ import { Artifacts, Templates } from './shelf.js';
|
|
|
49
49
|
import { Find } from './find.js';
|
|
50
50
|
import { DOCS, Slack, Who } from './foot.js';
|
|
51
51
|
import { Settings } from './settings/index.js';
|
|
52
|
+
import { Pitch } from './pitch.js';
|
|
53
|
+
import { Bar } from './visit.js';
|
|
52
54
|
function Shell() {
|
|
53
55
|
const host = useHost();
|
|
54
56
|
const t = useTarget();
|
|
@@ -70,12 +72,31 @@ function Shell() {
|
|
|
70
72
|
list.sort((a, b) => Number(b.status === 'running') - Number(a.status === 'running'));
|
|
71
73
|
return list;
|
|
72
74
|
}, [recents.value, order]);
|
|
73
|
-
const screen = r.kind === 'screen' ? r.screen : '';
|
|
74
75
|
// The top left, as on claude.ai: the builder's name, which leads to New, and under
|
|
75
76
|
// it the search that finds a run. The organization is the account's, so it is
|
|
76
77
|
// switched from the account menu at the foot. Beside the rail from md up, beside
|
|
77
78
|
// the drawer's button below it — one place at a time.
|
|
78
|
-
const name =
|
|
79
|
+
const name = _jsx(Name, { onPress: () => go('') });
|
|
80
|
+
const { links, more } = nav(host, go);
|
|
81
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), notice: host.person && !slack ? (_jsx(Slack, { onOpen: () => go(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) })) : undefined, account: host.person
|
|
82
|
+
? {
|
|
83
|
+
// Who, and in which organization, as claude.ai says who and on which plan.
|
|
84
|
+
name: [host.person.name || host.person.email, host.org].filter(Boolean).join(' · '),
|
|
85
|
+
onPress: () => {
|
|
86
|
+
setDrawer(false);
|
|
87
|
+
setMenu(true);
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
: { name: 'Sign in', onPress: () => host.signIn?.() }, collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 92, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(YStack, { position: "absolute", t: 10, l: 8, r: 8, z: 2, gap: "$2.5", "$max-md": { display: 'none' }, children: [_jsx(XStack, { px: "$2.5", children: name }), _jsxs(XStack, { render: "button", "aria-label": "Search runs", onPress: () => setFinding(true), items: "center", gap: "$2", px: "$2.5", height: 34, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 15, color: "$soft" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Search" })] })] })), host.person ? _jsx(Who, { open: menu, onOpenChange: setMenu, narrow: collapsed }) : null] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 44, px: "$2", gap: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }), name, _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setFinding(true), "aria-label": "Search runs", children: _jsx(Search, { size: 18 }) })] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The builder's places, as a rail draws them: four in view and the rest under
|
|
94
|
+
* More. Every one moves the builder through `go`, so a host that draws its own
|
|
95
|
+
* rail lists the same places, in the same words, as the builder's rail does.
|
|
96
|
+
*/
|
|
97
|
+
export function nav(host, go) {
|
|
98
|
+
const r = route(host.path);
|
|
99
|
+
const screen = r.kind === 'screen' ? r.screen : '';
|
|
79
100
|
const links = [
|
|
80
101
|
{ id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
|
|
81
102
|
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
@@ -104,16 +125,32 @@ function Shell() {
|
|
|
104
125
|
},
|
|
105
126
|
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
|
|
106
127
|
];
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
return { links, more };
|
|
129
|
+
}
|
|
130
|
+
/** The wordmark: the product's name, as its host calls it, which leads to New. */
|
|
131
|
+
function Name({ onPress }) {
|
|
132
|
+
const { name } = useHost();
|
|
133
|
+
return (_jsx(XStack, { render: "button", "aria-label": name, items: "center", onPress: onPress, children: _jsx(SizableText, { size: "$5", fontWeight: "600", color: "$ink", numberOfLines: 1, children: name }) }));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A visitor: no runs to list, so the product's header stands where the rail
|
|
137
|
+
* would. On New the composer fills the first screen and the pitch follows it.
|
|
138
|
+
*/
|
|
139
|
+
function Visit() {
|
|
140
|
+
const host = useHost();
|
|
141
|
+
const r = route(host.path);
|
|
142
|
+
// An address that names a part of the pitch (/#features) opens scrolled to it.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const id = window.location.hash.slice(1);
|
|
145
|
+
if (id)
|
|
146
|
+
document.getElementById(id)?.scrollIntoView();
|
|
147
|
+
}, []);
|
|
148
|
+
const start = () => {
|
|
149
|
+
const page = document.getElementById('visit');
|
|
150
|
+
page?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
151
|
+
page?.querySelector('textarea, [contenteditable="true"], input[type="text"]')?.focus();
|
|
152
|
+
};
|
|
153
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsx(Bar, { name: _jsx(Name, { onPress: () => host.go('') }) }), r.kind === 'new' ? (_jsxs(YStack, { id: "visit", flex: 1, minH: 0, overflow: "scroll", children: [_jsx(YStack, { shrink: 0, style: { height: 'calc(100dvh - 60px)' }, children: _jsx(Pane, {}) }), _jsx(Pitch, { onStart: start })] })) : (_jsx(Pane, {}))] }));
|
|
117
154
|
}
|
|
118
155
|
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
119
156
|
function Pane({ onStarted }) {
|
|
@@ -143,7 +180,7 @@ function Pane({ onStarted }) {
|
|
|
143
180
|
function Brand({ org }) {
|
|
144
181
|
if (!org || org === 'hanzo')
|
|
145
182
|
return _jsx(HanzoMark, { size: 18 });
|
|
146
|
-
const letter = org.
|
|
183
|
+
const letter = org.charAt(0).toUpperCase();
|
|
147
184
|
return (_jsx(XStack, { width: 18, height: 18, rounded: "$1", items: "center", justify: "center", bg: "$raised", "aria-hidden": true, children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: letter }) }));
|
|
148
185
|
}
|
|
149
186
|
function Screens({ rail }) {
|
|
@@ -151,7 +188,12 @@ function Screens({ rail }) {
|
|
|
151
188
|
const r = route(host.path);
|
|
152
189
|
if (r.kind === 'project')
|
|
153
190
|
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
154
|
-
|
|
191
|
+
if (!rail)
|
|
192
|
+
return _jsx(Pane, {});
|
|
193
|
+
// Signed out with no token to wait on: a token still resolving is a person, not a visitor.
|
|
194
|
+
if (!host.person && !host.token() && host.signIn)
|
|
195
|
+
return _jsx(Visit, {});
|
|
196
|
+
return _jsx(Shell, {});
|
|
155
197
|
}
|
|
156
198
|
/**
|
|
157
199
|
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
package/lib/choice.js
CHANGED
|
@@ -29,7 +29,7 @@ function write(org, value) {
|
|
|
29
29
|
/** Point New at a forge codebase, and optionally a first draft. */
|
|
30
30
|
export function pinCodebase(org, c, ask = '') {
|
|
31
31
|
const repo = asRepo(c);
|
|
32
|
-
write(org, { ...read(org), repo, branch: repo.default_branch
|
|
32
|
+
write(org, { ...read(org), repo, branch: repo.default_branch, ask });
|
|
33
33
|
}
|
|
34
34
|
/** Which board Issues is showing. '' is every board. */
|
|
35
35
|
export function pinBoard(org, key) {
|
|
@@ -39,7 +39,7 @@ export function pinBoard(org, key) {
|
|
|
39
39
|
catch {
|
|
40
40
|
/* Issues then shows every board */
|
|
41
41
|
}
|
|
42
|
-
window.dispatchEvent(new
|
|
42
|
+
window.dispatchEvent(new CustomEvent('hanzo-board', { detail: key }));
|
|
43
43
|
}
|
|
44
44
|
export const isForge = (repo) => Boolean(repo && repo.forge === true && repo.name);
|
|
45
45
|
const syncingKey = (org) => `hanzo.build.sync.${org ?? 'none'}`;
|
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/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/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/index.d.ts
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mount `<Builder host={…} />` under a gui root in a box with a height. The
|
|
5
5
|
* host says where the platform is, who is signed in, and how to move; the
|
|
6
|
-
* builder does everything else. hanzo.build hosts it whole
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* builder does everything else. hanzo.build hosts it whole. A host with a rail
|
|
7
|
+
* of its own mounts it with `rail={false}` and draws the builder's parts in that
|
|
8
|
+
* rail: its places (`nav`), its runs (`useSessions`, or `DevSection` for both),
|
|
9
|
+
* and at the foot the Slack card and the account menu (`Slack`, `Who`), under
|
|
10
|
+
* `HostProvider`.
|
|
9
11
|
*/
|
|
10
|
-
export { Builder } from './builder.tsx';
|
|
12
|
+
export { Builder, nav } from './builder.tsx';
|
|
11
13
|
export { DevSection, DOTS, useSessions } from './section.tsx';
|
|
14
|
+
export { HostProvider } from './host.tsx';
|
|
15
|
+
export { Slack, Who } from './foot.tsx';
|
|
12
16
|
export { administers } from './claims.ts';
|
|
13
17
|
export type { Host, Person } from './host.tsx';
|
|
14
18
|
export type { Session, Status } from './api/sessions.ts';
|
package/lib/index.js
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mount `<Builder host={…} />` under a gui root in a box with a height. The
|
|
5
5
|
* host says where the platform is, who is signed in, and how to move; the
|
|
6
|
-
* builder does everything else. hanzo.build hosts it whole
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* builder does everything else. hanzo.build hosts it whole. A host with a rail
|
|
7
|
+
* of its own mounts it with `rail={false}` and draws the builder's parts in that
|
|
8
|
+
* rail: its places (`nav`), its runs (`useSessions`, or `DevSection` for both),
|
|
9
|
+
* and at the foot the Slack card and the account menu (`Slack`, `Who`), under
|
|
10
|
+
* `HostProvider`.
|
|
9
11
|
*/
|
|
10
|
-
export { Builder } from './builder.js';
|
|
12
|
+
export { Builder, nav } from './builder.js';
|
|
11
13
|
export { DevSection, DOTS, useSessions } from './section.js';
|
|
14
|
+
export { HostProvider } from './host.js';
|
|
15
|
+
export { Slack, Who } from './foot.js';
|
|
12
16
|
export { administers } from './claims.js';
|
|
13
17
|
export { path, route, SESSION, SLUG } from './route.js';
|
package/lib/landing.js
CHANGED
|
@@ -6,8 +6,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* which repository and branch it starts from, and the ask itself.
|
|
7
7
|
* Under it: attach, dictate, the mode, and the model and effort on the right.
|
|
8
8
|
*
|
|
9
|
-
* Sending starts a coding run and opens it. The
|
|
10
|
-
*
|
|
9
|
+
* Sending starts a coding run and opens it. The codebase is optional: in the
|
|
10
|
+
* sandbox a run with none starts a new project named from the ask, as the
|
|
11
|
+
* platform does for a run that names no repository; a machine works in its own
|
|
12
|
+
* checkout, so a run there names one. The choices a person made here are kept in
|
|
13
|
+
* this browser, per org, so the next New starts where the last one did.
|
|
11
14
|
*/
|
|
12
15
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
13
16
|
import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
|
|
@@ -41,6 +44,13 @@ export const EFFORTS = [
|
|
|
41
44
|
];
|
|
42
45
|
const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium', ask: '' };
|
|
43
46
|
const COLUMN = 768;
|
|
47
|
+
/**
|
|
48
|
+
* What was typed here and not sent, for as long as this document lives. Signing
|
|
49
|
+
* in swaps a visitor's page for the rail, which draws New again in another
|
|
50
|
+
* place; the popup keeps the document so the sentence can come along, and this
|
|
51
|
+
* is where it waits.
|
|
52
|
+
*/
|
|
53
|
+
let unsent = { draft: '', files: [] };
|
|
44
54
|
export function Landing({ onStarted }) {
|
|
45
55
|
const host = useHost();
|
|
46
56
|
const t = useTarget();
|
|
@@ -50,8 +60,11 @@ export function Landing({ onStarted }) {
|
|
|
50
60
|
// Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
|
|
51
61
|
const kept = stored ?? { ...FIRST, ...prefs.code };
|
|
52
62
|
const set = (patch) => keep({ ...kept, ...patch });
|
|
53
|
-
const [draft, setDraft] = useState(kept.ask ||
|
|
54
|
-
const [files, setFiles] = useState(
|
|
63
|
+
const [draft, setDraft] = useState(kept.ask || unsent.draft);
|
|
64
|
+
const [files, setFiles] = useState(unsent.files);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
unsent = { draft, files };
|
|
67
|
+
}, [draft, files]);
|
|
55
68
|
const [busy, setBusy] = useState(false);
|
|
56
69
|
const [note, setNote] = useState('');
|
|
57
70
|
const [adding, setAdding] = useState(null);
|
|
@@ -81,35 +94,36 @@ export function Landing({ onStarted }) {
|
|
|
81
94
|
known.current.set(r.name, r);
|
|
82
95
|
return { repos, next: null };
|
|
83
96
|
}, [t]);
|
|
97
|
+
// Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
|
|
98
|
+
const home = kept.repo?.default_branch || 'main';
|
|
84
99
|
const loadBranches = useCallback(async (q) => {
|
|
85
|
-
|
|
86
|
-
return { branches: [], next: null };
|
|
87
|
-
const found = await one(t, kept.repo.name);
|
|
100
|
+
const found = await one(t, codebase);
|
|
88
101
|
const needle = q.trim().toLowerCase();
|
|
89
|
-
const names = (found?.branches ?? [
|
|
102
|
+
const names = (found?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
|
|
90
103
|
return { branches: names.map((name) => ({ name })), next: null };
|
|
91
|
-
}, [t,
|
|
104
|
+
}, [t, codebase, home]);
|
|
105
|
+
// The composer sends only a draft with words in it, and not while a send is out.
|
|
92
106
|
const send = async () => {
|
|
93
|
-
const prompt = draft.trim();
|
|
94
|
-
if (!prompt || busy)
|
|
95
|
-
return;
|
|
96
107
|
if (!signed) {
|
|
97
108
|
host.signIn?.();
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
const repo = isForge(kept.repo) ? kept.repo : null;
|
|
112
|
+
if (!repo && place.id) {
|
|
113
|
+
setNote(`A run on ${place.label} works in its own checkout: choose the codebase, or run it in Cloud.`);
|
|
102
114
|
return;
|
|
103
115
|
}
|
|
104
116
|
setBusy(true);
|
|
105
117
|
setNote('');
|
|
106
118
|
try {
|
|
107
119
|
const run = await start(t, {
|
|
108
|
-
prompt: compose(
|
|
120
|
+
prompt: compose(draft.trim(), files),
|
|
109
121
|
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
// No codebase is not sent, and the run starts a new project.
|
|
123
|
+
// An empty base or place is not sent: the default branch, and the sandbox.
|
|
124
|
+
repo: repo?.name,
|
|
125
|
+
base: repo ? kept.branch : undefined,
|
|
126
|
+
targetId: place.id,
|
|
113
127
|
mode: kept.mode,
|
|
114
128
|
model: kept.model === ENSO ? undefined : kept.model,
|
|
115
129
|
effort: kept.effort,
|
|
@@ -119,7 +133,7 @@ export function Landing({ onStarted }) {
|
|
|
119
133
|
onStarted(run.session);
|
|
120
134
|
}
|
|
121
135
|
catch (e) {
|
|
122
|
-
setNote(e
|
|
136
|
+
setNote(e.message);
|
|
123
137
|
}
|
|
124
138
|
finally {
|
|
125
139
|
setBusy(false);
|
|
@@ -146,7 +160,7 @@ export function Landing({ onStarted }) {
|
|
|
146
160
|
place: p,
|
|
147
161
|
})), [places.value, setUp]);
|
|
148
162
|
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)
|
|
163
|
+
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
164
|
const row = known.current.get(r.name) ?? chosen(r);
|
|
151
165
|
set({ repo: row, branch: row.default_branch });
|
|
152
166
|
}, load: loadRepos, troubleshoot: {
|
|
@@ -163,7 +177,7 @@ export function Landing({ onStarted }) {
|
|
|
163
177
|
name: r.name,
|
|
164
178
|
});
|
|
165
179
|
},
|
|
166
|
-
}, placeholder: "
|
|
180
|
+
}, placeholder: "New project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
|
|
167
181
|
const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, onChange: (m) => set({ 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: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
|
|
168
182
|
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", px: "$4", pb: "$10", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, gap: "$5", children: [_jsxs(XStack, { role: "heading", "aria-level": 1, justify: "center", items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(HanzoMark, { size: 26 }), _jsx(SizableText, { size: "$8", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' })] }), _jsxs(YStack, { gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
|
|
169
183
|
? `A proposed environment for ${codebase} is waiting for review.`
|
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
|
+
}
|