@hanzo/build 0.2.0 → 0.2.2
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 +26 -7
- package/lib/api/coding.d.ts +10 -13
- package/lib/api/coding.js +12 -15
- package/lib/api/turn.js +3 -0
- package/lib/builder.d.ts +8 -2
- package/lib/builder.js +31 -13
- package/lib/claims.d.ts +13 -0
- package/lib/claims.js +30 -0
- package/lib/host.d.ts +5 -5
- package/lib/host.js +5 -5
- package/lib/index.d.ts +7 -2
- package/lib/index.js +5 -2
- package/lib/landing.js +1 -1
- package/lib/project.js +1 -1
- package/lib/section.d.ts +20 -0
- package/lib/section.js +47 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
The builder is one component, `<Builder host={…} />` from `@hanzo/build`, with
|
|
7
|
+
three hosts: this page (hanzo.build), and the Dev section of the Hanzo app at
|
|
8
|
+
hanzo.ai/dev and hanzo.app/dev. None of them forks it.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
pnpm install
|
|
@@ -42,6 +42,25 @@ under the mount (`path`) and how to move (`go`), and where to link out
|
|
|
42
42
|
(`links`, `open`). Peers: `@hanzo/ui`, `@hanzo/gui`, `@hanzogui/lucide-icons-2`,
|
|
43
43
|
`react`.
|
|
44
44
|
|
|
45
|
+
### In a host that has its own rail
|
|
46
|
+
|
|
47
|
+
The Hanzo app already has a left column, so it mounts `<Builder host={host}
|
|
48
|
+
rail={false} />` in its pane and lists the builder in its own rail:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { Builder, DevSection } from '@hanzo/build'
|
|
52
|
+
|
|
53
|
+
<Sidebar> {/* the host's rail, @hanzo/ui/chat */}
|
|
54
|
+
<DevSection host={host} /> {/* New run, Artifacts, Templates, the runs */}
|
|
55
|
+
</Sidebar>
|
|
56
|
+
<Builder host={host} rail={false} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`DevSection` is rows of `@hanzo/ui/chat`'s Sidebar; `useSessions(host)` is its
|
|
60
|
+
data — the org's coding runs, kept live by the org's feed — for a host that
|
|
61
|
+
draws the rows itself. `administers(token, org)` reads the org-admin bit off a
|
|
62
|
+
hanzo.id token for `Host.admin`. One left column, never two.
|
|
63
|
+
|
|
45
64
|
## Addresses
|
|
46
65
|
|
|
47
66
|
| path | screen |
|
|
@@ -57,10 +76,10 @@ collide.
|
|
|
57
76
|
|
|
58
77
|
## What is on the screen
|
|
59
78
|
|
|
60
|
-
**The rail
|
|
61
|
-
(Templates, Machines, Docs), then the org's coding runs newest
|
|
62
|
-
live status dot, and the account. Collapse is an explicit toggle
|
|
63
|
-
browser.
|
|
79
|
+
**The rail** (hanzo.build). New, Artifacts, Customize (the platform's
|
|
80
|
+
plugins), More (Templates, Machines, Docs), then the org's coding runs newest
|
|
81
|
+
first with a live status dot, and the account. Collapse is an explicit toggle
|
|
82
|
+
kept in this browser. A host with its own rail draws `DevSection` there instead.
|
|
64
83
|
|
|
65
84
|
**New.** "What's up next?", and at the foot the composer: where the run runs
|
|
66
85
|
(Default is the platform's sandbox; the org's machines follow), the repository
|
package/lib/api/coding.d.ts
CHANGED
|
@@ -10,22 +10,19 @@
|
|
|
10
10
|
* id routes it to one of the org's machines, and `routed` says whether it got
|
|
11
11
|
* there.
|
|
12
12
|
*
|
|
13
|
-
* `mode
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* `mode` is what the run may do: `build` edits, commits and pushes its branch;
|
|
14
|
+
* `plan` reads the repository with a credential that can only read and answers
|
|
15
|
+
* with a plan, as its final status. A plan runs in the platform's sandbox only —
|
|
16
|
+
* a claimed machine clones and pushes with its own credential — so a plan
|
|
17
|
+
* routed to one is refused, here before sending as on the platform.
|
|
18
|
+
*
|
|
19
|
+
* `model` and `effort` are taken by the dev harness the builder runs (and by
|
|
20
|
+
* codex); the builder names no other harness, so they are always honoured.
|
|
17
21
|
*/
|
|
18
22
|
import { type Target } from './call.ts';
|
|
19
23
|
export type Mode = 'build' | 'plan';
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
* a plan run would edit, commit and push like any other — on a machine, with
|
|
23
|
-
* that machine's credentials. Until the platform takes `plan`, a plan ask is
|
|
24
|
-
* refused here rather than sent to become a build.
|
|
25
|
-
*/
|
|
26
|
-
export declare const HONOURED: readonly Mode[];
|
|
27
|
-
/** Why a mode cannot be asked for yet, or '' when it can. */
|
|
28
|
-
export declare const unhonoured: (mode: Mode | undefined) => string;
|
|
24
|
+
/** Why this mode cannot run where it is sent, or '' when it can. */
|
|
25
|
+
export declare const unhonoured: (mode: Mode | undefined, target?: string) => string;
|
|
29
26
|
export interface Ask {
|
|
30
27
|
/** The task, in the words you would use with a colleague. */
|
|
31
28
|
prompt: string;
|
package/lib/api/coding.js
CHANGED
|
@@ -10,22 +10,19 @@
|
|
|
10
10
|
* id routes it to one of the org's machines, and `routed` says whether it got
|
|
11
11
|
* there.
|
|
12
12
|
*
|
|
13
|
-
* `mode
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* `mode` is what the run may do: `build` edits, commits and pushes its branch;
|
|
14
|
+
* `plan` reads the repository with a credential that can only read and answers
|
|
15
|
+
* with a plan, as its final status. A plan runs in the platform's sandbox only —
|
|
16
|
+
* a claimed machine clones and pushes with its own credential — so a plan
|
|
17
|
+
* routed to one is refused, here before sending as on the platform.
|
|
18
|
+
*
|
|
19
|
+
* `model` and `effort` are taken by the dev harness the builder runs (and by
|
|
20
|
+
* codex); the builder names no other harness, so they are always honoured.
|
|
17
21
|
*/
|
|
18
22
|
import { call, Refusal } from './call.js';
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* that machine's credentials. Until the platform takes `plan`, a plan ask is
|
|
23
|
-
* refused here rather than sent to become a build.
|
|
24
|
-
*/
|
|
25
|
-
export const HONOURED = ['build'];
|
|
26
|
-
/** Why a mode cannot be asked for yet, or '' when it can. */
|
|
27
|
-
export const unhonoured = (mode) => mode && !HONOURED.includes(mode)
|
|
28
|
-
? 'Plan needs the platform’s plan mode, which the coding API does not take yet — this ask would build. Switch to Build, or wait for Plan.'
|
|
23
|
+
/** Why this mode cannot run where it is sent, or '' when it can. */
|
|
24
|
+
export const unhonoured = (mode, target) => mode === 'plan' && target?.trim()
|
|
25
|
+
? 'A plan runs in the Hanzo sandbox: a machine clones and pushes with its own credential. Choose Default, or switch to Build.'
|
|
29
26
|
: '';
|
|
30
27
|
/** The request body: only what was asked, with no empty strings sent as values. */
|
|
31
28
|
export function body(ask) {
|
|
@@ -40,7 +37,7 @@ export function body(ask) {
|
|
|
40
37
|
export async function start(t, ask) {
|
|
41
38
|
if (!ask.prompt.trim())
|
|
42
39
|
throw new Refusal(400, 'Say what the run should do');
|
|
43
|
-
const why = unhonoured(ask.mode);
|
|
40
|
+
const why = unhonoured(ask.mode, ask.targetId);
|
|
44
41
|
if (why)
|
|
45
42
|
throw new Refusal(400, why);
|
|
46
43
|
const r = await call(t, 'POST', '/v1/agent/coding', body(ask));
|
package/lib/api/turn.js
CHANGED
|
@@ -31,6 +31,9 @@ function status(b) {
|
|
|
31
31
|
case 'routed':
|
|
32
32
|
return 'Sent to a machine';
|
|
33
33
|
case 'done': {
|
|
34
|
+
// A plan's answer IS its final status: the run read and wrote nothing.
|
|
35
|
+
if (str(b.mode) === 'plan')
|
|
36
|
+
return str(b.plan) || 'Planned — the run answered with no plan';
|
|
34
37
|
if (b.changed === false)
|
|
35
38
|
return 'Done — nothing to change';
|
|
36
39
|
const pr = str(b.pr);
|
package/lib/builder.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type Host } from './host.tsx';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
|
4
|
+
*
|
|
5
|
+
* `rail={false}` leaves the left column to the host, which lists the builder's
|
|
6
|
+
* places and runs in its own rail with `DevSection`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Builder({ host, rail }: {
|
|
4
9
|
host: Host;
|
|
10
|
+
rail?: boolean;
|
|
5
11
|
}): import("react").JSX.Element;
|
package/lib/builder.js
CHANGED
|
@@ -14,6 +14,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
*
|
|
15
15
|
* A project's workspace takes the whole window — it has its own chat column,
|
|
16
16
|
* bar and panes — and its mark leads back here.
|
|
17
|
+
*
|
|
18
|
+
* A host with a rail of its own mounts `<Builder rail={false}>` and draws the
|
|
19
|
+
* builder's places and runs in that rail with `DevSection` (section.tsx): one
|
|
20
|
+
* left column, never two.
|
|
17
21
|
*/
|
|
18
22
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
19
23
|
import { Blocks, BookOpen, Cpu, LayoutTemplate, Menu, Puzzle } from '@hanzogui/lucide-icons-2';
|
|
@@ -27,15 +31,9 @@ import { Landing } from './landing.js';
|
|
|
27
31
|
import { Project } from './project.js';
|
|
28
32
|
import { path, route } from './route.js';
|
|
29
33
|
import { Run } from './run.js';
|
|
34
|
+
import { DOTS } from './section.js';
|
|
30
35
|
import { Artifacts, Templates } from './shelf.js';
|
|
31
36
|
import { Account, Find } from './account.js';
|
|
32
|
-
const DOTS = {
|
|
33
|
-
running: 'running',
|
|
34
|
-
paused: 'paused',
|
|
35
|
-
done: 'done',
|
|
36
|
-
stopped: 'stopped',
|
|
37
|
-
error: 'error',
|
|
38
|
-
};
|
|
39
37
|
function Shell() {
|
|
40
38
|
const host = useHost();
|
|
41
39
|
const t = useTarget();
|
|
@@ -68,14 +66,34 @@ function Shell() {
|
|
|
68
66
|
];
|
|
69
67
|
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", 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.' }), account: host.person
|
|
70
68
|
? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
|
|
71
|
-
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$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 }) }) }),
|
|
69
|
+
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$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 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
|
|
70
|
+
}
|
|
71
|
+
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
72
|
+
function Pane({ onStarted }) {
|
|
73
|
+
const host = useHost();
|
|
74
|
+
const r = route(host.path);
|
|
75
|
+
if (r.kind === 'run')
|
|
76
|
+
return _jsx(Run, { id: r.id }, r.id);
|
|
77
|
+
if (r.kind === 'screen')
|
|
78
|
+
return r.screen === 'artifacts' ? _jsx(Artifacts, {}) : _jsx(Templates, {});
|
|
79
|
+
return (_jsx(Landing, { onStarted: (id) => {
|
|
80
|
+
onStarted?.(id);
|
|
81
|
+
host.go(id);
|
|
82
|
+
} }));
|
|
72
83
|
}
|
|
73
|
-
function Screens() {
|
|
84
|
+
function Screens({ rail }) {
|
|
74
85
|
const host = useHost();
|
|
75
86
|
const r = route(host.path);
|
|
76
|
-
|
|
87
|
+
if (r.kind === 'project')
|
|
88
|
+
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
89
|
+
return rail ? _jsx(Shell, {}) : _jsx(Pane, {});
|
|
77
90
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
|
93
|
+
*
|
|
94
|
+
* `rail={false}` leaves the left column to the host, which lists the builder's
|
|
95
|
+
* places and runs in its own rail with `DevSection`.
|
|
96
|
+
*/
|
|
97
|
+
export function Builder({ host, rail = true }) {
|
|
98
|
+
return (_jsx(HostProvider, { host: host, children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: "$background", children: _jsx(Screens, { rail: rail }) }) }));
|
|
81
99
|
}
|
package/lib/claims.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a hanzo.id access token says, read without trusting it for anything the
|
|
3
|
+
* platform does not check again.
|
|
4
|
+
*/
|
|
5
|
+
/** The token's claims, or null when it is not a JWT. */
|
|
6
|
+
export declare function claims(token: string | null): Record<string, unknown> | null;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the token says its person administers `org`. Org admin, read off the
|
|
9
|
+
* token's own `orgs` claim — never the reserved `admin` org, which is platform
|
|
10
|
+
* authority and not what a builder decides anything by. The platform enforces
|
|
11
|
+
* it again; this only decides which publish path is offered.
|
|
12
|
+
*/
|
|
13
|
+
export declare function administers(token: string | null, org: string | null): boolean;
|
package/lib/claims.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a hanzo.id access token says, read without trusting it for anything the
|
|
3
|
+
* platform does not check again.
|
|
4
|
+
*/
|
|
5
|
+
/** The token's claims, or null when it is not a JWT. */
|
|
6
|
+
export function claims(token) {
|
|
7
|
+
if (!token)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const part = token.split('.')[1];
|
|
11
|
+
if (!part)
|
|
12
|
+
return null;
|
|
13
|
+
return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether the token says its person administers `org`. Org admin, read off the
|
|
21
|
+
* token's own `orgs` claim — never the reserved `admin` org, which is platform
|
|
22
|
+
* authority and not what a builder decides anything by. The platform enforces
|
|
23
|
+
* it again; this only decides which publish path is offered.
|
|
24
|
+
*/
|
|
25
|
+
export function administers(token, org) {
|
|
26
|
+
if (!org)
|
|
27
|
+
return false;
|
|
28
|
+
const set = claims(token)?.orgs;
|
|
29
|
+
return (Array.isArray(set) ? set : []).some((r) => r?.org === org && r.role === 'admin');
|
|
30
|
+
}
|
package/lib/host.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the builder needs from the page that mounts it.
|
|
3
3
|
*
|
|
4
|
-
* The builder is ONE component with
|
|
5
|
-
* signs in through Hanzo IAM and routes with react-router) and the
|
|
6
|
-
*
|
|
7
|
-
* Everything that differs between them is here,
|
|
8
|
-
* forks the builder and the builder imports
|
|
4
|
+
* The builder is ONE component with several hosts: its own page (hanzo.build,
|
|
5
|
+
* which signs in through Hanzo IAM and routes with react-router) and the Hanzo
|
|
6
|
+
* app's Dev section (hanzo.ai/dev and hanzo.app/dev, which already have a
|
|
7
|
+
* session, a router and a rail). Everything that differs between them is here,
|
|
8
|
+
* as values, so no host forks the builder and the builder imports no host.
|
|
9
9
|
*/
|
|
10
10
|
import { type ReactNode } from 'react';
|
|
11
11
|
import type { Target } from './api/call.ts';
|
package/lib/host.js
CHANGED
|
@@ -2,11 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* What the builder needs from the page that mounts it.
|
|
4
4
|
*
|
|
5
|
-
* The builder is ONE component with
|
|
6
|
-
* signs in through Hanzo IAM and routes with react-router) and the
|
|
7
|
-
*
|
|
8
|
-
* Everything that differs between them is here,
|
|
9
|
-
* forks the builder and the builder imports
|
|
5
|
+
* The builder is ONE component with several hosts: its own page (hanzo.build,
|
|
6
|
+
* which signs in through Hanzo IAM and routes with react-router) and the Hanzo
|
|
7
|
+
* app's Dev section (hanzo.ai/dev and hanzo.app/dev, which already have a
|
|
8
|
+
* session, a router and a rail). Everything that differs between them is here,
|
|
9
|
+
* as values, so no host forks the builder and the builder imports no host.
|
|
10
10
|
*/
|
|
11
11
|
import { createContext, useContext, useMemo } from 'react';
|
|
12
12
|
const Context = createContext(null);
|
package/lib/index.d.ts
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
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
|
|
7
|
-
*
|
|
6
|
+
* builder does everything else. hanzo.build hosts it whole; hanzo.ai and the
|
|
7
|
+
* Hanzo App mount it at /dev with `rail={false}` and list its places and runs in
|
|
8
|
+
* their own rail with `DevSection`.
|
|
8
9
|
*/
|
|
9
10
|
export { Builder } from './builder.tsx';
|
|
11
|
+
export { DevSection, DOTS, useSessions } from './section.tsx';
|
|
12
|
+
export { administers } from './claims.ts';
|
|
10
13
|
export type { Host, Person } from './host.tsx';
|
|
14
|
+
export type { Session, Status } from './api/sessions.ts';
|
|
15
|
+
export type { Read } from './data.ts';
|
|
11
16
|
export { path, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
|
package/lib/index.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
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
|
|
7
|
-
*
|
|
6
|
+
* builder does everything else. hanzo.build hosts it whole; hanzo.ai and the
|
|
7
|
+
* Hanzo App mount it at /dev with `rail={false}` and list its places and runs in
|
|
8
|
+
* their own rail with `DevSection`.
|
|
8
9
|
*/
|
|
9
10
|
export { Builder } from './builder.js';
|
|
11
|
+
export { DevSection, DOTS, useSessions } from './section.js';
|
|
12
|
+
export { administers } from './claims.js';
|
|
10
13
|
export { path, route, SESSION, SLUG } from './route.js';
|
package/lib/landing.js
CHANGED
|
@@ -128,5 +128,5 @@ export function Landing({ onStarted }) {
|
|
|
128
128
|
}),
|
|
129
129
|
}, placeholder: "Repository", disabled: !signed }), kept.repo ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
|
|
130
130
|
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 })] }));
|
|
131
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title: "What\u2019s up next?", mark: _jsx(HanzoMark, { size: 18 }), column: COLUMN }), _jsx(YStack, { flex: 1 }), _jsxs(YStack, { pb: "$2", gap: "$2", children: [note || unhonoured(kept.mode) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) })] }));
|
|
131
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title: "What\u2019s up next?", mark: _jsx(HanzoMark, { size: 18 }), column: COLUMN }), _jsx(YStack, { flex: 1 }), _jsxs(YStack, { pb: "$2", gap: "$2", children: [note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) })] }));
|
|
132
132
|
}
|
package/lib/project.js
CHANGED
|
@@ -239,7 +239,7 @@ export function Project({ slug }) {
|
|
|
239
239
|
}, empty: _jsxs(SizableText, { size: "$2", color: "$soft", p: "$4", children: ["Open a file from Files. Code here is read at ", ref, "; ask in the chat to change it."] }) })) : (_jsx(YStack, { p: "$4", gap: "$2", children: picked.length ? (picked.map((p) => (_jsxs(SizableText, { size: "$2", color: "$ink", children: [p.label, " \u2014 ", p.id] }, p.id)))) : (_jsx(SizableText, { size: "$2", color: "$soft", children: bridge
|
|
240
240
|
? 'Pick an element in the preview to see it here and attach it to your next ask.'
|
|
241
241
|
: 'Layers are read through the preview bridge, and this page does not run it. Its elements cannot be listed from here.' })) }));
|
|
242
|
-
return (_jsxs(_Fragment, { children: [_jsx(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name: project?.name ?? slug, onPress: () => setSwitcher(true) }), _jsx(XStack, { render: "button", "aria-label": "History", onPress: () => setHistory(true), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(Clock, { size: 16 }) }), _jsx(XStack, { render: "button", "aria-label": collapsed ? 'Show the chat' : 'Hide the chat', "aria-pressed": !collapsed, onPress: () => setCollapsed(!collapsed), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(PanelLeft, { size: 16 }) })] }), middle: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Views, { views: VIEWS, value: view, onChange: (v) => { setView(v); setPane('view'); }, label: "View" }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), _jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }), _jsxs(Button, { size: "sm", disabled: !project?.repo, onPress: () => project &&
|
|
242
|
+
return (_jsxs(_Fragment, { children: [_jsx(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name: project?.name ?? slug, onPress: () => setSwitcher(true) }), _jsx(XStack, { render: "button", "aria-label": "History", onPress: () => setHistory(true), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Clock, { size: 16 }) }), _jsx(XStack, { render: "button", "aria-label": collapsed ? 'Show the chat' : 'Hide the chat', "aria-pressed": !collapsed, onPress: () => setCollapsed(!collapsed), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(PanelLeft, { size: 16 }) })] }), middle: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Views, { views: VIEWS, value: view, onChange: (v) => { setView(v); setPane('view'); }, label: "View" }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), _jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", disabled: !project?.repo, onPress: () => project &&
|
|
243
243
|
setPublish({ repo: project.repo, title: project.name, ref, name: project.slug, project: project.slug }), children: [_jsx(Upload, { size: 14 }), " Publish"] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: collapsed, pane: pane, children: body }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", project?.name ?? slug] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
244
244
|
setChosen(r.id);
|
|
245
245
|
setHistory(false);
|
package/lib/section.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SessionStatus } from '@hanzo/ui/chat';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { Session } from './api/sessions.ts';
|
|
4
|
+
import { type Read } from './data.ts';
|
|
5
|
+
import type { Host } from './host.tsx';
|
|
6
|
+
/** A run's status, as the rail's dot draws it. Anything else is idle. */
|
|
7
|
+
export declare const DOTS: Record<string, SessionStatus>;
|
|
8
|
+
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
9
|
+
export declare function useSessions(host: Host): Read<Session[]>;
|
|
10
|
+
/**
|
|
11
|
+
* New, Artifacts, Templates, then the runs. `children` sit between the places
|
|
12
|
+
* and the runs — where a host lists its projects. `onPick` fires after a move,
|
|
13
|
+
* so a host that draws its rail as a drawer on a phone can close it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function DevSection({ host, onPick, label, children, }: {
|
|
16
|
+
host: Host;
|
|
17
|
+
onPick?: () => void;
|
|
18
|
+
label?: string;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}): import("react").JSX.Element;
|
package/lib/section.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The builder's places and runs, for a host that draws its own rail.
|
|
4
|
+
*
|
|
5
|
+
* A host that mounts `<Builder rail={false}>` keeps ONE left column — its own —
|
|
6
|
+
* and lists the builder in it with these. `useSessions` is the same read and
|
|
7
|
+
* the same org-wide feed the built-in rail uses, so a run started in the pane
|
|
8
|
+
* appears here with its live status. `DevSection` draws the builder's places
|
|
9
|
+
* and runs as rows of the host's sidebar (the `@hanzo/ui/chat` Sidebar the
|
|
10
|
+
* built-in rail is made of), and every row moves the builder through the
|
|
11
|
+
* host's own `go`.
|
|
12
|
+
*/
|
|
13
|
+
import { SizableText } from '@hanzo/gui';
|
|
14
|
+
import { Blocks, LayoutTemplate, SquarePen } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { useRecents } from './data.js';
|
|
18
|
+
import { path, route } from './route.js';
|
|
19
|
+
/** A run's status, as the rail's dot draws it. Anything else is idle. */
|
|
20
|
+
export const DOTS = {
|
|
21
|
+
running: 'running',
|
|
22
|
+
paused: 'paused',
|
|
23
|
+
done: 'done',
|
|
24
|
+
stopped: 'stopped',
|
|
25
|
+
error: 'error',
|
|
26
|
+
};
|
|
27
|
+
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
28
|
+
export function useSessions(host) {
|
|
29
|
+
const t = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
|
|
30
|
+
return useRecents(t, Boolean(host.person));
|
|
31
|
+
}
|
|
32
|
+
const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1", children: children }));
|
|
33
|
+
/**
|
|
34
|
+
* New, Artifacts, Templates, then the runs. `children` sit between the places
|
|
35
|
+
* and the runs — where a host lists its projects. `onPick` fires after a move,
|
|
36
|
+
* so a host that draws its rail as a drawer on a phone can close it.
|
|
37
|
+
*/
|
|
38
|
+
export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
39
|
+
const sessions = useSessions(host);
|
|
40
|
+
const r = route(host.path);
|
|
41
|
+
const go = (p) => {
|
|
42
|
+
host.go(p);
|
|
43
|
+
onPick?.();
|
|
44
|
+
};
|
|
45
|
+
const screen = r.kind === 'screen' ? r.screen : '';
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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 page (hanzo.build) and as a component (
|
|
6
|
+
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a page (hanzo.build) and as a component (hanzo.ai/dev).",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.18"
|
|
9
9
|
},
|