@hanzo/build 0.2.18 → 0.2.20
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 +16 -8
- package/lib/builder.d.ts +10 -0
- package/lib/builder.js +24 -39
- package/lib/index.d.ts +8 -4
- package/lib/index.js +8 -4
- package/lib/landing.d.ts +21 -0
- package/lib/landing.js +53 -19
- package/package.json +2 -2
- package/lib/pitch.d.ts +0 -4
- package/lib/pitch.js +0 -39
- package/lib/visit.d.ts +0 -31
- package/lib/visit.js +0 -53
package/README.md
CHANGED
|
@@ -3,17 +3,25 @@
|
|
|
3
3
|
Say what you want. Hanzo writes it, runs it, and puts it on a live URL — with
|
|
4
4
|
the database, the sign-in and the storage already there.
|
|
5
5
|
|
|
6
|
-
The builder is one component, `<Builder host={…} />` from `@hanzo/build
|
|
7
|
-
|
|
8
|
-
hanzo.
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
The builder is one component, `<Builder host={…} />` from `@hanzo/build`. The
|
|
7
|
+
Hanzo app at hanzo.ai mounts it as its Dev mode, and that is where people use
|
|
8
|
+
it: hanzo.build and build.hanzo.ai redirect there, to the same place. This
|
|
9
|
+
repository's own page is the development host — the same component, signed in
|
|
10
|
+
through hanzo.id — and a click stays on its origin, with github.com opened only
|
|
11
|
+
for the repository grant.
|
|
12
|
+
|
|
13
|
+
The page is signed-in only. A visitor is drawn nothing: the tab goes to
|
|
14
|
+
hanzo.id (no popup) and comes back to the address it asked for; hanzo.id
|
|
15
|
+
answers at once, with no screen, for a person already signed in there. Words
|
|
16
|
+
carried on arrival as `?q=` wait in New's composer, and a draft sent while
|
|
17
|
+
signed out is sent once its person is in (`landing.tsx` `hold`). It signs in as
|
|
18
|
+
`hanzo-app`, the Hanzo App's client.
|
|
11
19
|
|
|
12
20
|
```
|
|
13
21
|
pnpm install
|
|
14
22
|
pnpm dev # http://localhost:3200
|
|
15
23
|
pnpm test # the transport contract and the address grammar
|
|
16
|
-
pnpm site # every
|
|
24
|
+
pnpm site # every browser spec against the dev server (SITE= for another origin)
|
|
17
25
|
pnpm e2e # one live agent turn, projected the way a run draws it
|
|
18
26
|
pnpm cover # unit tests and every browser spec against a dev server, one coverage report in coverage/
|
|
19
27
|
pnpm build:lib # lib/ — what npm ships
|
|
@@ -100,7 +108,7 @@ collide.
|
|
|
100
108
|
|
|
101
109
|
## What is on the screen
|
|
102
110
|
|
|
103
|
-
**The rail** (
|
|
111
|
+
**The rail** (this page, or `rail` left on). At the top, the mark. Signed in to an organization,
|
|
104
112
|
that spot is the organization switcher: the org, and the projects under it.
|
|
105
113
|
New, Projects, Artifacts, Customize, Automations, then More (Codebase, Issues,
|
|
106
114
|
Templates, Machines, Docs), then the org's coding runs newest first with a live
|
|
@@ -193,7 +201,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
|
|
|
193
201
|
|
|
194
202
|
| call | what |
|
|
195
203
|
|---|---|
|
|
196
|
-
| `POST /v1/agent/coding` | start a run → 202 `{sessionId, …}` |
|
|
204
|
+
| `POST /v1/agent/coding` | start a run → 202 `{sessionId, …}`; with no `repo` the platform starts a new project named from the ask |
|
|
197
205
|
| `GET /v1/agent/sessions?kind=coding` | Recents, and a project's runs |
|
|
198
206
|
| `GET /v1/agent/sessions/{id}` · `GET /v1/agent/sessions/stream?root=` | a run, and its live feed (SSE over fetch) |
|
|
199
207
|
| `POST /v1/agent/sessions/{id}/message` · `/pause` · `/resume` · `/stop` | steer, pause, resume a machine's run, stop |
|
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 {
|
|
35
|
+
import { 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,8 +49,6 @@ 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';
|
|
54
52
|
function Shell() {
|
|
55
53
|
const host = useHost();
|
|
56
54
|
const t = useTarget();
|
|
@@ -72,12 +70,31 @@ function Shell() {
|
|
|
72
70
|
list.sort((a, b) => Number(b.status === 'running') - Number(a.status === 'running'));
|
|
73
71
|
return list;
|
|
74
72
|
}, [recents.value, order]);
|
|
75
|
-
const screen = r.kind === 'screen' ? r.screen : '';
|
|
76
73
|
// The top left, as on claude.ai: the builder's name, which leads to New, and under
|
|
77
74
|
// it the search that finds a run. The organization is the account's, so it is
|
|
78
75
|
// switched from the account menu at the foot. Beside the rail from md up, beside
|
|
79
76
|
// the drawer's button below it — one place at a time.
|
|
80
77
|
const name = _jsx(Name, { onPress: () => go('') });
|
|
78
|
+
const { links, more } = nav(host, go);
|
|
79
|
+
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
|
|
80
|
+
? {
|
|
81
|
+
// Who, and in which organization, as claude.ai says who and on which plan.
|
|
82
|
+
name: [host.person.name || host.person.email, host.org].filter(Boolean).join(' · '),
|
|
83
|
+
onPress: () => {
|
|
84
|
+
setDrawer(false);
|
|
85
|
+
setMenu(true);
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
: { 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); } })] }));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The builder's places, as a rail draws them: four in view and the rest under
|
|
92
|
+
* More. Every one moves the builder through `go`, so a host that draws its own
|
|
93
|
+
* rail lists the same places, in the same words, as the builder's rail does.
|
|
94
|
+
*/
|
|
95
|
+
export function nav(host, go) {
|
|
96
|
+
const r = route(host.path);
|
|
97
|
+
const screen = r.kind === 'screen' ? r.screen : '';
|
|
81
98
|
const links = [
|
|
82
99
|
{ id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
|
|
83
100
|
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
@@ -106,42 +123,13 @@ function Shell() {
|
|
|
106
123
|
},
|
|
107
124
|
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
|
|
108
125
|
];
|
|
109
|
-
return
|
|
110
|
-
? {
|
|
111
|
-
// Who, and in which organization, as claude.ai says who and on which plan.
|
|
112
|
-
name: [host.person.name || host.person.email, host.org].filter(Boolean).join(' · '),
|
|
113
|
-
onPress: () => {
|
|
114
|
-
setDrawer(false);
|
|
115
|
-
setMenu(true);
|
|
116
|
-
},
|
|
117
|
-
}
|
|
118
|
-
: { 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); } })] }));
|
|
126
|
+
return { links, more };
|
|
119
127
|
}
|
|
120
128
|
/** The wordmark: the product's name, as its host calls it, which leads to New. */
|
|
121
129
|
function Name({ onPress }) {
|
|
122
130
|
const { name } = useHost();
|
|
123
131
|
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 }) }));
|
|
124
132
|
}
|
|
125
|
-
/**
|
|
126
|
-
* A visitor: no runs to list, so the product's header stands where the rail
|
|
127
|
-
* would. On New the composer fills the first screen and the pitch follows it.
|
|
128
|
-
*/
|
|
129
|
-
function Visit() {
|
|
130
|
-
const host = useHost();
|
|
131
|
-
const r = route(host.path);
|
|
132
|
-
// An address that names a part of the pitch (/#features) opens scrolled to it.
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
const id = window.location.hash.slice(1);
|
|
135
|
-
if (id)
|
|
136
|
-
document.getElementById(id)?.scrollIntoView();
|
|
137
|
-
}, []);
|
|
138
|
-
const start = () => {
|
|
139
|
-
const page = document.getElementById('visit');
|
|
140
|
-
page?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
141
|
-
page?.querySelector('textarea, [contenteditable="true"], input[type="text"]')?.focus();
|
|
142
|
-
};
|
|
143
|
-
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, {}))] }));
|
|
144
|
-
}
|
|
145
133
|
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
146
134
|
function Pane({ onStarted }) {
|
|
147
135
|
const host = useHost();
|
|
@@ -180,9 +168,6 @@ function Screens({ rail }) {
|
|
|
180
168
|
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
181
169
|
if (!rail)
|
|
182
170
|
return _jsx(Pane, {});
|
|
183
|
-
// Signed out with no token to wait on: a token still resolving is a person, not a visitor.
|
|
184
|
-
if (!host.person && !host.token() && host.signIn)
|
|
185
|
-
return _jsx(Visit, {});
|
|
186
171
|
return _jsx(Shell, {});
|
|
187
172
|
}
|
|
188
173
|
/**
|
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.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Attached } from './tools.tsx';
|
|
1
2
|
/** One vocabulary for the mode, on New and in a workspace. */
|
|
2
3
|
export declare const MODES: readonly [{
|
|
3
4
|
readonly id: 'build';
|
|
@@ -18,6 +19,26 @@ export declare const EFFORTS: readonly [{
|
|
|
18
19
|
readonly id: 'high';
|
|
19
20
|
readonly label: 'High';
|
|
20
21
|
}];
|
|
22
|
+
/**
|
|
23
|
+
* What was typed here and not sent, kept in this tab.
|
|
24
|
+
*
|
|
25
|
+
* Signing in leaves the page for hanzo.id and comes back (app/enter.ts), and the
|
|
26
|
+
* sentence is still in the composer after. A draft its person SENT while signed
|
|
27
|
+
* out is sent on the return: pressing Send was the ask, and the sign-in stood
|
|
28
|
+
* between it and the run. Only this page writes `sent`, so no link can start a
|
|
29
|
+
* run on somebody's behalf — an address carrying words only fills the composer.
|
|
30
|
+
*
|
|
31
|
+
* sessionStorage, because a draft belongs to the tab it was typed in and not to
|
|
32
|
+
* the next person at this browser; a browser that refuses storage keeps it for
|
|
33
|
+
* the life of the page, as before.
|
|
34
|
+
*/
|
|
35
|
+
export interface Unsent {
|
|
36
|
+
draft: string;
|
|
37
|
+
files: Attached[];
|
|
38
|
+
sent?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Hold what New has unsent, or forget it once there is nothing. */
|
|
41
|
+
export declare function hold(u: Unsent): void;
|
|
21
42
|
export declare function Landing({ onStarted }: {
|
|
22
43
|
onStarted: (session: string) => void;
|
|
23
44
|
}): import("react").JSX.Element;
|
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,13 +44,32 @@ 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;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const UNSENT = 'hanzo.build.unsent';
|
|
48
|
+
let held = { draft: '', files: [] };
|
|
49
|
+
function unsent() {
|
|
50
|
+
try {
|
|
51
|
+
const v = JSON.parse(window.sessionStorage.getItem(UNSENT) ?? 'null');
|
|
52
|
+
if (v && typeof v.draft === 'string' && Array.isArray(v.files))
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* a browser that refuses storage, or a value that is not ours */
|
|
57
|
+
}
|
|
58
|
+
return held;
|
|
59
|
+
}
|
|
60
|
+
/** Hold what New has unsent, or forget it once there is nothing. */
|
|
61
|
+
export function hold(u) {
|
|
62
|
+
held = u;
|
|
63
|
+
try {
|
|
64
|
+
if (!u.draft && !u.files.length)
|
|
65
|
+
window.sessionStorage.removeItem(UNSENT);
|
|
66
|
+
else
|
|
67
|
+
window.sessionStorage.setItem(UNSENT, JSON.stringify(u));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* kept for the life of the page */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
51
73
|
export function Landing({ onStarted }) {
|
|
52
74
|
const host = useHost();
|
|
53
75
|
const t = useTarget();
|
|
@@ -57,11 +79,10 @@ export function Landing({ onStarted }) {
|
|
|
57
79
|
// Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
|
|
58
80
|
const kept = stored ?? { ...FIRST, ...prefs.code };
|
|
59
81
|
const set = (patch) => keep({ ...kept, ...patch });
|
|
60
|
-
const [
|
|
61
|
-
const [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}, [draft, files]);
|
|
82
|
+
const [was] = useState(unsent);
|
|
83
|
+
const [draft, setDraft] = useState(kept.ask || was.draft);
|
|
84
|
+
const [files, setFiles] = useState(was.files);
|
|
85
|
+
useEffect(() => hold({ draft, files }), [draft, files]);
|
|
65
86
|
const [busy, setBusy] = useState(false);
|
|
66
87
|
const [note, setNote] = useState('');
|
|
67
88
|
const [adding, setAdding] = useState(null);
|
|
@@ -102,11 +123,13 @@ export function Landing({ onStarted }) {
|
|
|
102
123
|
// The composer sends only a draft with words in it, and not while a send is out.
|
|
103
124
|
const send = async () => {
|
|
104
125
|
if (!signed) {
|
|
126
|
+
hold({ draft, files, sent: true });
|
|
105
127
|
host.signIn?.();
|
|
106
128
|
return;
|
|
107
129
|
}
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
const repo = isForge(kept.repo) ? kept.repo : null;
|
|
131
|
+
if (!repo && place.id) {
|
|
132
|
+
setNote(`A run on ${place.label} works in its own checkout: choose the codebase, or run it in Cloud.`);
|
|
110
133
|
return;
|
|
111
134
|
}
|
|
112
135
|
setBusy(true);
|
|
@@ -115,9 +138,10 @@ export function Landing({ onStarted }) {
|
|
|
115
138
|
const run = await start(t, {
|
|
116
139
|
prompt: compose(draft.trim(), files),
|
|
117
140
|
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
141
|
+
// No codebase is not sent, and the run starts a new project.
|
|
118
142
|
// An empty base or place is not sent: the default branch, and the sandbox.
|
|
119
|
-
repo:
|
|
120
|
-
base: kept.branch,
|
|
143
|
+
repo: repo?.name,
|
|
144
|
+
base: repo ? kept.branch : undefined,
|
|
121
145
|
targetId: place.id,
|
|
122
146
|
mode: kept.mode,
|
|
123
147
|
model: kept.model === ENSO ? undefined : kept.model,
|
|
@@ -134,6 +158,16 @@ export function Landing({ onStarted }) {
|
|
|
134
158
|
setBusy(false);
|
|
135
159
|
}
|
|
136
160
|
};
|
|
161
|
+
// A draft sent while signed out goes once its person is in, and only once.
|
|
162
|
+
const owed = useRef(was.sent === true);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!signed || !owed.current || !draft.trim())
|
|
165
|
+
return;
|
|
166
|
+
owed.current = false;
|
|
167
|
+
void send();
|
|
168
|
+
// Sent once, on the sign-in that it waited for.
|
|
169
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
170
|
+
}, [signed]);
|
|
137
171
|
/** Start the agent that finds this codebase's environment, and open it. A refusal is the dialog's to say. */
|
|
138
172
|
const setup = async () => {
|
|
139
173
|
const run = await start(t, { prompt: SETUP, repo: codebase, mode: 'setup' });
|
|
@@ -172,7 +206,7 @@ export function Landing({ onStarted }) {
|
|
|
172
206
|
name: r.name,
|
|
173
207
|
});
|
|
174
208
|
},
|
|
175
|
-
}, placeholder: "
|
|
209
|
+
}, 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 })] }));
|
|
176
210
|
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 })] }));
|
|
177
211
|
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'
|
|
178
212
|
? `A proposed environment for ${codebase} is waiting for review.`
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
|
-
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a
|
|
6
|
+
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.18"
|
|
9
9
|
},
|
package/lib/pitch.d.ts
DELETED
package/lib/pitch.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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/visit.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
export declare const PLACES: readonly [{
|
|
3
|
-
readonly label: 'Download';
|
|
4
|
-
readonly href: '/#features';
|
|
5
|
-
}, {
|
|
6
|
-
readonly label: 'Enterprise';
|
|
7
|
-
readonly href: "https://hanzo.ai/enterprise";
|
|
8
|
-
}, {
|
|
9
|
-
readonly label: 'Pricing';
|
|
10
|
-
readonly href: "https://hanzo.ai/pricing";
|
|
11
|
-
}];
|
|
12
|
-
export declare const RESOURCES: readonly [{
|
|
13
|
-
readonly label: 'Docs';
|
|
14
|
-
readonly href: 'https://docs.hanzo.ai';
|
|
15
|
-
}, {
|
|
16
|
-
readonly label: 'Blog';
|
|
17
|
-
readonly href: "https://hanzo.ai/blog";
|
|
18
|
-
}, {
|
|
19
|
-
readonly label: 'Customers';
|
|
20
|
-
readonly href: "https://hanzo.ai/customers";
|
|
21
|
-
}, {
|
|
22
|
-
readonly label: 'Learn';
|
|
23
|
-
readonly href: "https://hanzo.ai/learn";
|
|
24
|
-
}, {
|
|
25
|
-
readonly label: 'Support';
|
|
26
|
-
readonly href: "https://hanzo.ai/support";
|
|
27
|
-
}];
|
|
28
|
-
/** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
|
|
29
|
-
export declare function Bar({ name }: {
|
|
30
|
-
name: ReactNode;
|
|
31
|
-
}): import("react").JSX.Element;
|
package/lib/visit.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* The page to a visitor: the product's header over New.
|
|
4
|
-
*
|
|
5
|
-
* Signed out there are no runs to list, so the rail gives way to a header —
|
|
6
|
-
* Download (Hanzo Dev, under New, pitch.tsx), Resources, Enterprise, Pricing — and the two
|
|
7
|
-
* doors in, which are one door: IAM's page offers both. New stays the page, and
|
|
8
|
-
* sending from it asks the visitor to sign in with the sentence kept
|
|
9
|
-
* (landing.tsx). Below md the four places fold into one menu beside Log in.
|
|
10
|
-
*/
|
|
11
|
-
import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
-
import { ChevronDown, Menu } from '@hanzogui/lucide-icons-2';
|
|
13
|
-
import { Button } from '@hanzo/ui';
|
|
14
|
-
import { MenuLabel, MenuRow, MenuRule } from '@hanzo/ui/product';
|
|
15
|
-
import { useState } from 'react';
|
|
16
|
-
import { useHost } from './host.js';
|
|
17
|
-
const SITE = 'https://hanzo.ai';
|
|
18
|
-
export const PLACES = [
|
|
19
|
-
{ label: 'Download', href: '/#features' },
|
|
20
|
-
{ label: 'Enterprise', href: `${SITE}/enterprise` },
|
|
21
|
-
{ label: 'Pricing', href: `${SITE}/pricing` },
|
|
22
|
-
];
|
|
23
|
-
export const RESOURCES = [
|
|
24
|
-
{ label: 'Docs', href: 'https://docs.hanzo.ai' },
|
|
25
|
-
{ label: 'Blog', href: `${SITE}/blog` },
|
|
26
|
-
{ label: 'Customers', href: `${SITE}/customers` },
|
|
27
|
-
{ label: 'Learn', href: `${SITE}/learn` },
|
|
28
|
-
{ label: 'Support', href: `${SITE}/support` },
|
|
29
|
-
];
|
|
30
|
-
const leave = (href) => window.location.assign(href);
|
|
31
|
-
/** One place in the header: an address, drawn as text. */
|
|
32
|
-
function Place({ href, children }) {
|
|
33
|
-
return (_jsx("a", { href: href, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: children }) }) }));
|
|
34
|
-
}
|
|
35
|
-
function Resources() {
|
|
36
|
-
const [open, setOpen] = useState(false);
|
|
37
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-start", offset: 6, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs(XStack, { render: "button", "aria-label": "Resources", "aria-expanded": open, items: "center", gap: "$1", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Resources" }), _jsx(ChevronDown, { size: 14, color: "$soft" })] }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Resources", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 220, bg: "$panel", borderColor: "$borderColor", children: _jsx(YStack, { gap: "$1", children: RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))) }) })] }));
|
|
38
|
-
}
|
|
39
|
-
/** Below md: every place, the resources and the door, in one menu. */
|
|
40
|
-
function Fold({ onEnter }) {
|
|
41
|
-
const [open, setOpen] = useState(false);
|
|
42
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-end", offset: 6, stayInFrame: true, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Menu", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Menu", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [PLACES.map((p) => (_jsx(MenuRow, { label: p.label, onPress: () => leave(p.href) }, p.label))), _jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Resources" }), RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Sign up", onPress: () => {
|
|
43
|
-
setOpen(false);
|
|
44
|
-
onEnter();
|
|
45
|
-
} })] }) })] }));
|
|
46
|
-
}
|
|
47
|
-
/** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
|
|
48
|
-
export function Bar({ name }) {
|
|
49
|
-
const host = useHost();
|
|
50
|
-
const enter = () => host.signIn?.();
|
|
51
|
-
const [features, enterprise, pricing] = PLACES;
|
|
52
|
-
return (_jsxs(XStack, { role: "banner", height: 60, px: "$4", gap: "$2", items: "center", shrink: 0, $md: { px: '$6' }, children: [name, _jsxs(XStack, { role: "navigation", "aria-label": "Site", items: "center", gap: "$1", ml: "$4", "$max-md": { display: 'none' }, children: [_jsx(Place, { href: features.href, children: features.label }), _jsx(Resources, {}), _jsx(Place, { href: enterprise.href, children: enterprise.label }), _jsx(Place, { href: pricing.href, children: pricing.label })] }), _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "sm", onPress: enter, children: "Log in" }), _jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Button, { size: "sm", variant: "primary", onPress: enter, children: "Sign up" }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Fold, { onEnter: enter }) })] }));
|
|
53
|
-
}
|