@hanzo/build 0.2.6 → 0.2.8
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 +12 -5
- package/lib/account.js +2 -13
- package/lib/api/mcp.d.ts +19 -0
- package/lib/api/mcp.js +46 -0
- package/lib/builder.js +5 -27
- package/lib/forge.js +4 -12
- package/lib/host.d.ts +4 -0
- package/lib/mcp.d.ts +1 -0
- package/lib/mcp.js +28 -0
- package/lib/route.d.ts +1 -1
- package/lib/route.js +1 -1
- package/lib/section.js +2 -2
- package/lib/switch.d.ts +1 -0
- package/lib/switch.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ hanzo.id token for `Host.admin`. One left column, never two.
|
|
|
70
70
|
| `''` | New — the empty state and the composer |
|
|
71
71
|
| `sess_<32 hex>` | one run, live |
|
|
72
72
|
| `-/automations` | repeating work, read from `/v1/auto/flows` |
|
|
73
|
+
| `-/mcp` | the fleet's native MCP servers, from `POST /v1/mcp` `tools/list` |
|
|
73
74
|
| `-/codebases` | the forge's repositories |
|
|
74
75
|
| `-/sync` | bring granted repositories onto the forge |
|
|
75
76
|
| `-/projects` | the forge's boards |
|
|
@@ -83,11 +84,16 @@ collide.
|
|
|
83
84
|
|
|
84
85
|
## What is on the screen
|
|
85
86
|
|
|
86
|
-
**The rail** (hanzo.build).
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
**The rail** (hanzo.build). At the top, the mark. Signed in to an organization,
|
|
88
|
+
that spot is the organization switcher: the org, and the projects under it.
|
|
89
|
+
New, then Automations, then the forge — Codebase, Projects, Issues — then MCP,
|
|
90
|
+
Artifacts, More (Templates, Machines, Docs), then the org's coding runs newest
|
|
91
|
+
first with a live status dot. The account stays at the foot. Settings opens
|
|
92
|
+
that account on this page. Collapse is an explicit toggle kept in this browser.
|
|
93
|
+
A host with its own rail draws `DevSection` there instead.
|
|
94
|
+
|
|
95
|
+
**MCP.** The native servers `POST /v1/mcp` lists, each with the operations it
|
|
96
|
+
names. A run starts a server when it calls it, and the server stays up afterwards.
|
|
91
97
|
|
|
92
98
|
**New.** "What's up next?", and at the foot the composer: where the run runs
|
|
93
99
|
(Default is the platform's sandbox; the org's machines follow), the codebase
|
|
@@ -123,6 +129,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
|
|
|
123
129
|
| `GET /v1/projects` · `POST /v1/projects/fork` · `GET /v1/templates` | artifacts, templates |
|
|
124
130
|
| `GET /v1/git/repos/{name}/tree` · `/blob` | Files and Code |
|
|
125
131
|
| `POST /v1/platform/apps` · `GET /v1/platform/builds` | Add to project, Publish |
|
|
132
|
+
| `POST /v1/mcp` | the native MCP servers, `tools/list` |
|
|
126
133
|
| `GET /v1/models` · `POST /v1/event` | the model list, a verdict |
|
|
127
134
|
|
|
128
135
|
`mode`, `model` and `effort` are sent with a run as asked; the platform
|
package/lib/account.js
CHANGED
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* finding a run by name (the search icon).
|
|
5
5
|
*/
|
|
6
6
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
7
|
-
import { LogOut
|
|
7
|
+
import { LogOut } from '@hanzogui/lucide-icons-2';
|
|
8
8
|
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
9
9
|
import { useMemo, useState } from 'react';
|
|
10
10
|
import { useHost } from './host.js';
|
|
@@ -20,21 +20,10 @@ export function onAccount(fn) {
|
|
|
20
20
|
export function revealAccount() {
|
|
21
21
|
reveal?.();
|
|
22
22
|
}
|
|
23
|
-
/** A settings address on another host. This page's own account is the dialog itself. */
|
|
24
|
-
function accountSettings(href) {
|
|
25
|
-
if (typeof window === 'undefined')
|
|
26
|
-
return false;
|
|
27
|
-
try {
|
|
28
|
-
return new URL(href, window.location.origin).origin !== window.location.origin;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
23
|
export function Account({ open, onOpenChange }) {
|
|
35
24
|
const host = useHost();
|
|
36
25
|
const who = host.person;
|
|
37
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }),
|
|
26
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsx(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null })] }) }));
|
|
38
27
|
}
|
|
39
28
|
export function Find({ open, onOpenChange, recents, onOpen, }) {
|
|
40
29
|
const [q, setQ] = useState('');
|
package/lib/api/mcp.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fleet's native MCP servers, as the MCP server itself lists them.
|
|
3
|
+
*
|
|
4
|
+
* POST /v1/mcp {"method":"tools/list"} → {result:{tools:[…]}}
|
|
5
|
+
*
|
|
6
|
+
* One tool is one native server. Its operations are the names in `op`. A run
|
|
7
|
+
* starts a server the first time it calls it; the server stays up afterwards.
|
|
8
|
+
* `describe` is how a caller reads one operation's schema, so it is not a server.
|
|
9
|
+
*/
|
|
10
|
+
import { type Target } from './call.ts';
|
|
11
|
+
export interface Native {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
ops: string[];
|
|
15
|
+
}
|
|
16
|
+
/** The servers in one tools/list answer. A missing tool array is a refusal. */
|
|
17
|
+
export declare function nativesOf(body: unknown): Native[];
|
|
18
|
+
/** Every native server the fleet MCP server is serving. */
|
|
19
|
+
export declare function natives(t: Target): Promise<Native[]>;
|
package/lib/api/mcp.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fleet's native MCP servers, as the MCP server itself lists them.
|
|
3
|
+
*
|
|
4
|
+
* POST /v1/mcp {"method":"tools/list"} → {result:{tools:[…]}}
|
|
5
|
+
*
|
|
6
|
+
* One tool is one native server. Its operations are the names in `op`. A run
|
|
7
|
+
* starts a server the first time it calls it; the server stays up afterwards.
|
|
8
|
+
* `describe` is how a caller reads one operation's schema, so it is not a server.
|
|
9
|
+
*/
|
|
10
|
+
import { call, Refusal } from './call.js';
|
|
11
|
+
const words = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x !== '') : []);
|
|
12
|
+
/** The servers in one tools/list answer. A missing tool array is a refusal. */
|
|
13
|
+
export function nativesOf(body) {
|
|
14
|
+
const root = body && typeof body === 'object' ? body : {};
|
|
15
|
+
const err = root.error;
|
|
16
|
+
if (err && typeof err === 'object') {
|
|
17
|
+
const message = err.message;
|
|
18
|
+
throw new Refusal(200, typeof message === 'string' && message ? message : 'The MCP server refused the list');
|
|
19
|
+
}
|
|
20
|
+
const result = root.result && typeof root.result === 'object' ? root.result : null;
|
|
21
|
+
const tools = result && Array.isArray(result.tools) ? result.tools : null;
|
|
22
|
+
if (!tools)
|
|
23
|
+
throw new Refusal(502, 'The MCP server did not list its tools');
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const raw of tools) {
|
|
26
|
+
if (!raw || typeof raw !== 'object')
|
|
27
|
+
continue;
|
|
28
|
+
const t = raw;
|
|
29
|
+
const name = typeof t.name === 'string' ? t.name : '';
|
|
30
|
+
if (!name || name === 'describe')
|
|
31
|
+
continue;
|
|
32
|
+
const schema = t.inputSchema && typeof t.inputSchema === 'object' ? t.inputSchema : {};
|
|
33
|
+
const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
|
|
34
|
+
const op = props.op && typeof props.op === 'object' ? props.op : {};
|
|
35
|
+
out.push({
|
|
36
|
+
name,
|
|
37
|
+
description: typeof t.description === 'string' ? t.description : '',
|
|
38
|
+
ops: words(op.enum),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Every native server the fleet MCP server is serving. */
|
|
44
|
+
export async function natives(t) {
|
|
45
|
+
return nativesOf(await call(t, 'POST', '/v1/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list' }));
|
|
46
|
+
}
|
package/lib/builder.js
CHANGED
|
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* /-/issues the forge's issues
|
|
12
12
|
* /-/artifacts what this org has built
|
|
13
13
|
* /-/templates the public starters
|
|
14
|
+
* /-/mcp the fleet's native MCP servers
|
|
14
15
|
* /<slug> a deployed project's workspace, in this same window
|
|
15
16
|
*
|
|
16
17
|
* The rail is the sessions rail every Hanzo surface shares: New, the builder's
|
|
@@ -41,6 +42,7 @@ import { Run } from './run.js';
|
|
|
41
42
|
import { DOTS } from './section.js';
|
|
42
43
|
import { Artifacts, Templates } from './shelf.js';
|
|
43
44
|
import { Account, Find, onAccount } from './account.js';
|
|
45
|
+
import { Where } from './switch.js';
|
|
44
46
|
function Shell() {
|
|
45
47
|
const host = useHost();
|
|
46
48
|
const t = useTarget();
|
|
@@ -78,7 +80,7 @@ function Shell() {
|
|
|
78
80
|
active: screen === 'issues',
|
|
79
81
|
},
|
|
80
82
|
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
81
|
-
{ id: '
|
|
83
|
+
{ id: 'mcp', label: 'MCP', icon: _jsx(Puzzle, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), active: screen === 'mcp' },
|
|
82
84
|
];
|
|
83
85
|
const more = [
|
|
84
86
|
{ id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
|
|
@@ -86,37 +88,13 @@ function Shell() {
|
|
|
86
88
|
id: 'machines',
|
|
87
89
|
label: 'Machines',
|
|
88
90
|
icon: _jsx(Cpu, { size: 16 }),
|
|
89
|
-
onPress: () =>
|
|
90
|
-
const home = host.links.home.replace(/\/+$/, '');
|
|
91
|
-
const computers = `${home}/platform/computers`;
|
|
92
|
-
try {
|
|
93
|
-
if (new URL(computers, window.location.origin).origin === window.location.origin) {
|
|
94
|
-
host.go('');
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
host.open(computers);
|
|
102
|
-
},
|
|
91
|
+
onPress: () => host.go(''),
|
|
103
92
|
},
|
|
104
93
|
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open('https://docs.hanzo.ai/docs/dev', '_blank', 'noopener,noreferrer') },
|
|
105
94
|
];
|
|
106
95
|
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.' }), account: host.person
|
|
107
96
|
? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
|
|
108
|
-
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => {
|
|
109
|
-
try {
|
|
110
|
-
if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
|
|
111
|
-
setAccount(true);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
host.open(host.links.settings);
|
|
119
|
-
}, onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _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); } })] }));
|
|
97
|
+
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => setAccount(true), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : host.person && (host.org || (host.memberships?.length ?? 0) > 0) ? 80 : 40, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsx(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", children: host.person && (host.org || (host.memberships?.length ?? 0) > 0) ? (_jsx(Where, {})) : (_jsx(XStack, { render: "button", "aria-label": "Hanzo", items: "center", onPress: () => go(''), children: _jsx(HanzoMark, { size: 18 }) })) }))] }), _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); } })] }));
|
|
120
98
|
}
|
|
121
99
|
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
122
100
|
function Pane({ onStarted }) {
|
package/lib/forge.js
CHANGED
|
@@ -18,6 +18,7 @@ import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './ch
|
|
|
18
18
|
import { useKept, useRead } from './data.js';
|
|
19
19
|
import { useHost, useTarget } from './host.js';
|
|
20
20
|
import { path } from './route.js';
|
|
21
|
+
import { Servers } from './mcp.js';
|
|
21
22
|
import { Sync } from './sync.js';
|
|
22
23
|
import { grants } from './api/github.js';
|
|
23
24
|
function Head({ title, says }) {
|
|
@@ -41,6 +42,8 @@ export function Forge({ screen }) {
|
|
|
41
42
|
return _jsx(Automations, {});
|
|
42
43
|
if (screen === 'sync')
|
|
43
44
|
return _jsx(Sync, {});
|
|
45
|
+
if (screen === 'mcp')
|
|
46
|
+
return _jsx(Servers, {});
|
|
44
47
|
if (screen === 'codebases')
|
|
45
48
|
return _jsx(Codebases, {});
|
|
46
49
|
if (screen === 'projects')
|
|
@@ -170,18 +173,7 @@ function Codebases() {
|
|
|
170
173
|
pinCodebase(host.org, c);
|
|
171
174
|
host.go('');
|
|
172
175
|
};
|
|
173
|
-
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", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => {
|
|
174
|
-
try {
|
|
175
|
-
if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
|
|
176
|
-
revealAccount();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
host.open(host.links.settings);
|
|
184
|
-
}, children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
|
|
176
|
+
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", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => revealAccount(), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
|
|
185
177
|
const row = await create(t, name, description);
|
|
186
178
|
pinCodebase(host.org, row);
|
|
187
179
|
setMaking(false);
|
package/lib/host.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface Host {
|
|
|
22
22
|
token: () => string | null;
|
|
23
23
|
/** The org every call is scoped to. */
|
|
24
24
|
org: string | null;
|
|
25
|
+
/** The organizations this person belongs to. The switcher lists these and no others. */
|
|
26
|
+
memberships?: readonly string[];
|
|
27
|
+
/** Stay on this page and scope every read to `org`. */
|
|
28
|
+
chooseOrg?: (org: string) => void;
|
|
25
29
|
/** Who is signed in, or null. */
|
|
26
30
|
person: Person | null;
|
|
27
31
|
/** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
|
package/lib/mcp.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Servers(): import("react").JSX.Element;
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The fleet's native MCP servers.
|
|
4
|
+
*
|
|
5
|
+
* The list is the MCP server's own tools/list. A server starts when a run
|
|
6
|
+
* calls it and stays up afterwards, so this pane lists them rather than
|
|
7
|
+
* starting them.
|
|
8
|
+
*/
|
|
9
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
10
|
+
import { ChevronDown } from '@hanzogui/lucide-icons-2';
|
|
11
|
+
import { Input } from '@hanzo/ui';
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { natives } from './api/mcp.js';
|
|
14
|
+
import { useRead } from './data.js';
|
|
15
|
+
import { useTarget } from './host.js';
|
|
16
|
+
export function Servers() {
|
|
17
|
+
const t = useTarget();
|
|
18
|
+
const list = useRead(() => natives(t), [], [t]);
|
|
19
|
+
const [q, setQ] = useState('');
|
|
20
|
+
const [open, setOpen] = useState(null);
|
|
21
|
+
const needle = q.trim().toLowerCase();
|
|
22
|
+
const shown = list.value.filter((s) => !needle || s.name.toLowerCase().includes(needle) || s.ops.some((op) => op.toLowerCase().includes(needle)));
|
|
23
|
+
const ops = shown.reduce((n, s) => n + s.ops.length, 0);
|
|
24
|
+
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: "MCP" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Native servers. A run starts one when it calls it, and the server stays up afterwards." })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find a server or an operation\u2026", "aria-label": "Find a server" }), list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the servers\u2026" })) : list.error && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : shown.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.value.length === 0 ? 'The MCP server listed no native servers.' : 'No server matches.' })) : (_jsxs(YStack, { gap: "$1", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " servers \u00B7 ", ops, " operations"] }), shown.map((s) => {
|
|
25
|
+
const on = open === s.name;
|
|
26
|
+
return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, onPress: () => setOpen(on ? null : s.name), children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: s.name }), s.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: on ? undefined : 2, children: s.description })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: s.ops.length }), _jsx(ChevronDown, { size: 14, style: { transform: on ? 'rotate(180deg)' : undefined } })] }), on ? (_jsx(YStack, { px: "$3", py: "$2", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: s.ops.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "This server named no operations." })) : (s.ops.map((op) => (_jsx(SizableText, { size: "$1", color: "$ink", children: op }, op)))) })) : null] }, s.name));
|
|
27
|
+
})] }))] }) }));
|
|
28
|
+
}
|
package/lib/route.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* so it can hold neither the `_` a session id carries nor start with `-`.
|
|
11
11
|
* Anything else is not an address here and reads as the empty state.
|
|
12
12
|
*/
|
|
13
|
-
export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync';
|
|
13
|
+
export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp';
|
|
14
14
|
export type Route = {
|
|
15
15
|
kind: 'new';
|
|
16
16
|
} | {
|
package/lib/route.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const SESSION = /^sess_[0-9a-f]{32}$/;
|
|
2
2
|
export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
3
|
-
const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync'];
|
|
3
|
+
const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync', 'mcp'];
|
|
4
4
|
export function route(path) {
|
|
5
5
|
const p = path.replace(/^\/+|\/+$/g, '');
|
|
6
6
|
if (SESSION.test(p))
|
package/lib/section.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* host's own `go`.
|
|
12
12
|
*/
|
|
13
13
|
import { SizableText } from '@hanzo/gui';
|
|
14
|
-
import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
14
|
+
import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, Puzzle, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
15
15
|
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
17
|
import { pinBoard } from './choice.js';
|
|
@@ -48,5 +48,5 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
|
48
48
|
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(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === 'codebases', onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), children: "Codebase" }), _jsx(SidebarItem, { icon: _jsx(Kanban, { size: 16, "aria-hidden": true }), active: screen === 'projects', onPress: () => go(path({ kind: 'screen', screen: 'projects' })), children: "Projects" }), _jsx(SidebarItem, { icon: _jsx(CircleDot, { size: 16, "aria-hidden": true }), active: screen === 'issues', onPress: () => {
|
|
49
49
|
pinBoard(host.org, '');
|
|
50
50
|
go(path({ kind: 'screen', screen: 'issues' }));
|
|
51
|
-
}, children: "Issues" }), _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.' })) })] }));
|
|
51
|
+
}, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), active: screen === 'mcp', onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), children: "MCP" }), _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.' })) })] }));
|
|
52
52
|
}
|
package/lib/switch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Where(): import("react").JSX.Element;
|
package/lib/switch.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The rail's top left: the organization, and the projects under it.
|
|
4
|
+
*
|
|
5
|
+
* The account stays at the foot of the rail. Settings opens that account on
|
|
6
|
+
* this page. Choosing an organization reloads this origin so every read is
|
|
7
|
+
* scoped to it.
|
|
8
|
+
*/
|
|
9
|
+
import { YStack } from '@hanzo/gui';
|
|
10
|
+
import { MenuLabel, MenuRow, MenuRule, OrgSwitcher } from '@hanzo/ui/product';
|
|
11
|
+
import { revealAccount } from './account.js';
|
|
12
|
+
import { projects } from './api/projects.js';
|
|
13
|
+
import { useRead } from './data.js';
|
|
14
|
+
import { useHost, useTarget } from './host.js';
|
|
15
|
+
import { route } from './route.js';
|
|
16
|
+
export function Where() {
|
|
17
|
+
const host = useHost();
|
|
18
|
+
const t = useTarget();
|
|
19
|
+
const signed = Boolean(host.person);
|
|
20
|
+
const list = useRead(signed ? () => projects(t) : null, [], [t, signed]);
|
|
21
|
+
const here = route(host.path);
|
|
22
|
+
const current = host.org || '';
|
|
23
|
+
const scope = {
|
|
24
|
+
currentOrg: () => current,
|
|
25
|
+
setCurrentOrg: (next) => host.chooseOrg?.(next),
|
|
26
|
+
isScopedAway: () => false,
|
|
27
|
+
hasSelectedOrg: () => Boolean(current),
|
|
28
|
+
enterOrg: (next) => host.chooseOrg?.(next),
|
|
29
|
+
leaveOrg: () => undefined,
|
|
30
|
+
switchOrg: (next) => host.chooseOrg?.(next),
|
|
31
|
+
};
|
|
32
|
+
const rows = () => Promise.resolve((host.memberships ?? (current ? [current] : [])).map((name) => ({ name, displayName: name })));
|
|
33
|
+
return (_jsx(OrgSwitcher, { scope: scope, orgs: (page) => (page === 0 ? rows() : Promise.resolve([])), pageSize: 50, search: false, heading: "Organization", lead: true, sub: here.kind === 'project' ? here.slug : undefined, current: current ? { name: current, displayName: current } : undefined, aria: current ? `Organization ${current}` : 'Organization', footer: (close) => (_jsxs(YStack, { gap: "$1", children: [list.value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Project" }), _jsx(YStack, { role: "radiogroup", "aria-label": "Projects", gap: "$1", children: list.value.slice(0, 12).map((p) => (_jsx(MenuRow, { label: p.name || p.slug, active: here.kind === 'project' && here.slug === p.slug, onPress: () => {
|
|
34
|
+
close();
|
|
35
|
+
host.go(p.slug);
|
|
36
|
+
} }, p.slug))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", onPress: () => {
|
|
37
|
+
close();
|
|
38
|
+
revealAccount();
|
|
39
|
+
} })] })) }));
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
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).",
|