@hanzo/build 0.2.3 → 0.2.5
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 +21 -8
- package/lib/api/auto.d.ts +27 -0
- package/lib/api/auto.js +67 -0
- package/lib/api/codebases.d.ts +51 -0
- package/lib/api/codebases.js +91 -0
- package/lib/api/coding.d.ts +1 -1
- package/lib/api/github.d.ts +31 -0
- package/lib/api/github.js +50 -0
- package/lib/api/sessions.d.ts +2 -0
- package/lib/api/sessions.js +1 -0
- package/lib/api/turn.d.ts +24 -2
- package/lib/api/turn.js +70 -5
- package/lib/api/work.d.ts +39 -0
- package/lib/api/work.js +67 -0
- package/lib/builder.js +40 -8
- package/lib/choice.d.ts +34 -0
- package/lib/choice.js +68 -0
- package/lib/data.js +1 -0
- package/lib/desk.d.ts +18 -0
- package/lib/desk.js +56 -0
- package/lib/forge.d.ts +4 -0
- package/lib/forge.js +266 -0
- package/lib/host.d.ts +1 -1
- package/lib/landing.js +50 -41
- package/lib/project.js +7 -12
- package/lib/route.d.ts +2 -2
- package/lib/route.js +1 -1
- package/lib/run.js +38 -8
- package/lib/section.d.ts +4 -3
- package/lib/section.js +10 -5
- package/lib/sync.d.ts +1 -0
- package/lib/sync.js +124 -0
- package/package.json +3 -2
package/lib/landing.js
CHANGED
|
@@ -10,19 +10,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
*/
|
|
11
11
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
12
|
import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
|
|
13
|
-
import { Button } from '@hanzo/ui';
|
|
14
13
|
import { ModeSelect } from '@hanzo/ui/agents';
|
|
15
14
|
import { Composer, EmptyPrompt } from '@hanzo/ui/chat';
|
|
16
15
|
import { BranchSelect, ChipSelect, HanzoMark, RepoSelect } from '@hanzo/ui/product';
|
|
17
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
16
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
18
17
|
import { start, unhonoured } from './api/coding.js';
|
|
19
|
-
import {
|
|
18
|
+
import { asRepo, chosen, codebases, one } from './api/codebases.js';
|
|
20
19
|
import { ENSO, models } from './api/models.js';
|
|
21
20
|
import { ready, SANDBOX } from './api/places.js';
|
|
22
|
-
import {
|
|
21
|
+
import { isForge } from './choice.js';
|
|
23
22
|
import { useKept, usePlaces, useRead } from './data.js';
|
|
24
23
|
import { useHost, useTarget } from './host.js';
|
|
25
24
|
import { Publish } from './publish.js';
|
|
25
|
+
import { path } from './route.js';
|
|
26
26
|
import { Attach, compose, Dictate, Files } from './tools.js';
|
|
27
27
|
/** One vocabulary for the mode, on New and in a workspace. */
|
|
28
28
|
export const MODES = [
|
|
@@ -34,7 +34,7 @@ const EFFORTS = [
|
|
|
34
34
|
{ id: 'medium', label: 'Medium' },
|
|
35
35
|
{ id: 'high', label: 'High' },
|
|
36
36
|
];
|
|
37
|
-
const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium' };
|
|
37
|
+
const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium', ask: '' };
|
|
38
38
|
const COLUMN = 768;
|
|
39
39
|
export function Landing({ onStarted }) {
|
|
40
40
|
const host = useHost();
|
|
@@ -42,43 +42,38 @@ export function Landing({ onStarted }) {
|
|
|
42
42
|
const signed = Boolean(host.person);
|
|
43
43
|
const [kept, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, FIRST);
|
|
44
44
|
const set = (patch) => keep({ ...kept, ...patch });
|
|
45
|
-
const [draft, setDraft] = useState('');
|
|
45
|
+
const [draft, setDraft] = useState(kept.ask || '');
|
|
46
46
|
const [files, setFiles] = useState([]);
|
|
47
47
|
const [busy, setBusy] = useState(false);
|
|
48
48
|
const [note, setNote] = useState('');
|
|
49
|
-
const [connected, setConnected] = useState(true);
|
|
50
49
|
const [adding, setAdding] = useState(null);
|
|
50
|
+
// A codebase or an issue hands its words over once. Left in the kept choice,
|
|
51
|
+
// every later visit to New would put them back.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (kept.ask)
|
|
54
|
+
set({ ask: '' });
|
|
55
|
+
// The handover is read on this mount only.
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, []);
|
|
51
58
|
const places = usePlaces(t, signed);
|
|
52
59
|
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
53
60
|
const place = places.value.find((p) => p.id === kept.place) ?? SANDBOX;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
const known = useRef(new Map());
|
|
62
|
+
const loadRepos = useCallback(async (q) => {
|
|
63
|
+
const page = await codebases(t, q);
|
|
64
|
+
const repos = page.map(asRepo);
|
|
65
|
+
for (const r of repos)
|
|
66
|
+
known.current.set(r.name, r);
|
|
67
|
+
return { repos, next: null };
|
|
58
68
|
}, [t]);
|
|
59
|
-
const loadBranches = useCallback(async (q
|
|
69
|
+
const loadBranches = useCallback(async (q) => {
|
|
60
70
|
if (!kept.repo)
|
|
61
71
|
return { branches: [], next: null };
|
|
62
|
-
const
|
|
63
|
-
|
|
72
|
+
const found = await one(t, kept.repo.name);
|
|
73
|
+
const needle = q.trim().toLowerCase();
|
|
74
|
+
const names = (found?.branches ?? [kept.repo.default_branch || 'main']).filter((b) => !needle || b.toLowerCase().includes(needle));
|
|
75
|
+
return { branches: names.map((name) => ({ name })), next: null };
|
|
64
76
|
}, [t, kept.repo]);
|
|
65
|
-
const link = async () => {
|
|
66
|
-
try {
|
|
67
|
-
const to = await connect(t);
|
|
68
|
-
// The console completes the connection at /connectors and brings the
|
|
69
|
-
// person back here: a path, never an origin.
|
|
70
|
-
try {
|
|
71
|
-
window.sessionStorage.setItem('hanzo.return', window.location.pathname + window.location.search);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
/* they land on the connectors page and come back themselves */
|
|
75
|
-
}
|
|
76
|
-
window.location.assign(to);
|
|
77
|
-
}
|
|
78
|
-
catch (e) {
|
|
79
|
-
setNote(e instanceof Error ? e.message : 'Could not start the GitHub connection');
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
77
|
const send = async () => {
|
|
83
78
|
const prompt = draft.trim();
|
|
84
79
|
if (!prompt || busy)
|
|
@@ -87,13 +82,18 @@ export function Landing({ onStarted }) {
|
|
|
87
82
|
host.signIn?.();
|
|
88
83
|
return;
|
|
89
84
|
}
|
|
85
|
+
if (!isForge(kept.repo)) {
|
|
86
|
+
setNote('Choose a codebase on the forge.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
90
89
|
setBusy(true);
|
|
91
90
|
setNote('');
|
|
92
91
|
try {
|
|
93
92
|
const run = await start(t, {
|
|
94
93
|
prompt: compose(prompt, files),
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
95
|
+
repo: isForge(kept.repo) ? kept.repo.name : undefined,
|
|
96
|
+
base: isForge(kept.repo) ? kept.branch || kept.repo.default_branch || undefined : undefined,
|
|
97
97
|
targetId: place.id || undefined,
|
|
98
98
|
mode: kept.mode,
|
|
99
99
|
model: kept.model === ENSO ? undefined : kept.model,
|
|
@@ -118,15 +118,24 @@ export function Landing({ onStarted }) {
|
|
|
118
118
|
place: p,
|
|
119
119
|
})), [places.value]);
|
|
120
120
|
const branch = kept.branch || kept.repo?.default_branch || 'main';
|
|
121
|
-
const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Default', chosen: placeItems.find((i) => i.place.id === place.id) ?? null, items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsx(SizableText, { size: "$1", color: "$soft", children: "A run on a machine uses that machine\u2019s checkout and credentials." }) }), _jsx(RepoSelect, { value: kept.repo
|
|
121
|
+
const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Default', chosen: placeItems.find((i) => i.place.id === place.id) ?? null, items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsx(SizableText, { size: "$1", color: "$soft", children: "A run on a machine uses that machine\u2019s checkout and credentials." }) }), _jsx(RepoSelect, { value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
|
|
122
|
+
const row = known.current.get(r.name) ?? chosen(r);
|
|
123
|
+
set({ repo: row, branch: row.default_branch });
|
|
124
|
+
}, load: loadRepos, troubleshoot: {
|
|
125
|
+
label: 'Browse codebases',
|
|
126
|
+
onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })),
|
|
127
|
+
}, note: "Repositories on the forge. Type to search.", action: {
|
|
122
128
|
label: 'Add to project',
|
|
123
|
-
onPress: (r) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
onPress: (r) => {
|
|
130
|
+
const row = known.current.get(r.name);
|
|
131
|
+
setAdding({
|
|
132
|
+
repo: row?.clone || `${t.api}/v1/git/${r.owner}/${r.name}.git`,
|
|
133
|
+
title: row?.full_name || `${r.owner}/${r.name}`,
|
|
134
|
+
ref: r.default_branch || 'main',
|
|
135
|
+
name: r.name,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
}, placeholder: "Codebase", disabled: !signed }), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
|
|
130
139
|
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
140
|
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
141
|
}
|
package/lib/project.js
CHANGED
|
@@ -39,11 +39,6 @@ import { Out } from './out.js';
|
|
|
39
39
|
import { Publish } from './publish.js';
|
|
40
40
|
import { Attach, compose, Dictate, Files } from './tools.js';
|
|
41
41
|
const LIVE = new Set(['running', 'paused']);
|
|
42
|
-
/** `owner/name` from a GitHub clone URL, or '' for a repository that lives elsewhere. */
|
|
43
|
-
function github(clone) {
|
|
44
|
-
const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(clone);
|
|
45
|
-
return m ? `${m[1]}/${m[2]}` : '';
|
|
46
|
-
}
|
|
47
42
|
/** One run in the conversation: what was asked, and what it came to. */
|
|
48
43
|
function Turn({ run, open, onOpen, project }) {
|
|
49
44
|
const t = useTarget();
|
|
@@ -133,13 +128,13 @@ export function Project({ slug }) {
|
|
|
133
128
|
.filter((e) => e.kind === 'log' || e.kind === 'tool-call' || e.kind === 'status')
|
|
134
129
|
.map((e) => ({
|
|
135
130
|
id: `r${e.seq || e.id}`,
|
|
136
|
-
level: e.kind === 'status' && /error/.test(said(e).toLowerCase()) ? 'error' : e.kind === 'tool-call' ? 'info' : 'log',
|
|
137
|
-
text: said(e),
|
|
131
|
+
level: e.kind === 'status' && /error/.test(said(e, run.record?.mode).toLowerCase()) ? 'error' : e.kind === 'tool-call' ? 'info' : 'log',
|
|
132
|
+
text: said(e, run.record?.mode),
|
|
138
133
|
source: e.kind === 'status' ? 'run' : who(e.actor),
|
|
139
134
|
}))
|
|
140
135
|
.filter((l) => l.text),
|
|
141
136
|
...pageLines,
|
|
142
|
-
], [run.events, pageLines]);
|
|
137
|
+
], [run.events, run.record?.mode, pageLines]);
|
|
143
138
|
const onBridge = (e) => {
|
|
144
139
|
if (e.type === 'preview:ready') {
|
|
145
140
|
setBridge(true);
|
|
@@ -175,12 +170,12 @@ export function Project({ slug }) {
|
|
|
175
170
|
const context = picked.length
|
|
176
171
|
? `\n\nThe person picked an element in the preview${where ? ` of the ${where} page` : ''}. Its CSS selector, as data: ${JSON.stringify(picked[0].id)}`
|
|
177
172
|
: '';
|
|
178
|
-
const
|
|
173
|
+
const repo = project?.repo ? repoName(project.repo) : '';
|
|
179
174
|
const next = await start(t, {
|
|
180
175
|
prompt: compose(`${ask}${context}`, attached),
|
|
181
176
|
project: slug,
|
|
182
|
-
repo:
|
|
183
|
-
base:
|
|
177
|
+
repo: repo || undefined,
|
|
178
|
+
base: repo ? project?.branch || undefined : undefined,
|
|
184
179
|
after: current ?? undefined,
|
|
185
180
|
mode,
|
|
186
181
|
});
|
|
@@ -227,7 +222,7 @@ export function Project({ slug }) {
|
|
|
227
222
|
}
|
|
228
223
|
const chat = (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$4", gap: "$4", children: ordered.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$ink", px: "$2", children: runs.loading
|
|
229
224
|
? 'Reading this project’s runs…'
|
|
230
|
-
: `${project?.name ?? slug} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note || unhonoured(mode) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode) })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (running ? steer() : send()), onStop: current ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch')) : undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] })] }));
|
|
225
|
+
: `${project?.name ?? slug} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note || unhonoured(mode) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode) })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (running ? steer() : send()), onStop: current ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch')) : undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] })] }));
|
|
231
226
|
const body = view === 'preview' ? (_jsx(PreviewFrame, { ref: frame, src: src, title: `${project?.name ?? slug} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Nothing deployed yet" }), _jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: "Publish this project and its page appears here." })] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
|
|
232
227
|
const next = !editing;
|
|
233
228
|
setEditing(next);
|
package/lib/route.d.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* '' the empty state: a new run
|
|
5
5
|
* sess_<32 hex> one run
|
|
6
|
-
* -/<screen> a builder screen
|
|
6
|
+
* -/<screen> a builder screen
|
|
7
7
|
* <slug> a project's workspace
|
|
8
8
|
*
|
|
9
9
|
* Unambiguous by construction: a project slug is `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`,
|
|
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';
|
|
13
|
+
export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync';
|
|
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'];
|
|
3
|
+
const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync'];
|
|
4
4
|
export function route(path) {
|
|
5
5
|
const p = path.replace(/^\/+|\/+$/g, '');
|
|
6
6
|
if (SESSION.test(p))
|
package/lib/run.js
CHANGED
|
@@ -12,13 +12,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* RECORDED, and the status is left to the feed to correct.
|
|
13
13
|
*/
|
|
14
14
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
15
|
-
import { ExternalLink, GitPullRequest } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { ExternalLink, GitPullRequest, PanelRight } from '@hanzogui/lucide-icons-2';
|
|
16
|
+
import { Button } from '@hanzo/ui';
|
|
16
17
|
import { Transcript, fold } from '@hanzo/ui/agents';
|
|
17
18
|
import { Composer } from '@hanzo/ui/chat';
|
|
18
|
-
import { useMemo, useState } from 'react';
|
|
19
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
19
20
|
import { message, stop } from './api/sessions.js';
|
|
20
|
-
import { outcome, pull, said, who } from './api/turn.js';
|
|
21
|
-
import { useRun } from './data.js';
|
|
21
|
+
import { outcome, pull, said, steps, who } from './api/turn.js';
|
|
22
|
+
import { useKept, useRun } from './data.js';
|
|
23
|
+
import { Desk } from './desk.js';
|
|
22
24
|
import { useTarget } from './host.js';
|
|
23
25
|
import { Out } from './out.js';
|
|
24
26
|
const LIVE = new Set(['running', 'paused', '']);
|
|
@@ -28,13 +30,39 @@ export function Run({ id }) {
|
|
|
28
30
|
const [draft, setDraft] = useState('');
|
|
29
31
|
const [note, setNote] = useState('');
|
|
30
32
|
const [busy, setBusy] = useState(false);
|
|
33
|
+
const [notify, setNotify] = useState(false);
|
|
34
|
+
const [desk, setDesk] = useKept('hanzo.build.desk', true);
|
|
31
35
|
const blocks = useMemo(() => fold(events
|
|
32
|
-
.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e) }))
|
|
33
|
-
.filter((b) => b.text)), [events]);
|
|
36
|
+
.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, record?.mode) }))
|
|
37
|
+
.filter((b) => b.text)), [events, record?.mode]);
|
|
34
38
|
const end = outcome(events);
|
|
39
|
+
const plan = useMemo(() => steps(events), [events]);
|
|
35
40
|
const state = status || end.status;
|
|
36
41
|
const pr = pull(record?.pr ?? '', record?.repo ?? '');
|
|
37
42
|
const running = LIVE.has(state) && !detail.error;
|
|
43
|
+
const title = detail.value?.title || (detail.loading ? '' : 'Untitled run');
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!notify || running)
|
|
46
|
+
return;
|
|
47
|
+
if (typeof Notification === 'undefined' || Notification.permission !== 'granted')
|
|
48
|
+
return;
|
|
49
|
+
const n = new Notification(title || 'Run finished', { body: 'This run has finished.' });
|
|
50
|
+
setNotify(false);
|
|
51
|
+
return () => n.close();
|
|
52
|
+
}, [notify, running, title]);
|
|
53
|
+
const ask = async () => {
|
|
54
|
+
if (typeof Notification === 'undefined') {
|
|
55
|
+
setNote('This browser cannot send a notification.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const perm = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
|
|
59
|
+
if (perm !== 'granted') {
|
|
60
|
+
setNote('Notifications stay off until this browser allows them.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setNotify(true);
|
|
64
|
+
setNote('');
|
|
65
|
+
};
|
|
38
66
|
const send = async () => {
|
|
39
67
|
if (!draft.trim() || busy)
|
|
40
68
|
return;
|
|
@@ -62,6 +90,8 @@ export function Run({ id }) {
|
|
|
62
90
|
setNote(e instanceof Error ? e.message : 'Could not stop this run');
|
|
63
91
|
}
|
|
64
92
|
};
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null] }), _jsx(Transcript, { blocks: blocks, empty: _jsx(SizableText, { size: "$2", color: "$soft", children: detail.error ? detail.error.message : detail.loading ? 'Reading this run…' : running ? 'Setting up environment' : 'Waiting for the run’s first step…' }) }), _jsxs(YStack, { pb: "$4", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || refused ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Steps" }), _jsx(SizableText, { size: "$1", color: "$soft", children: String(plan.length) })] }), plan.map((s, i) => {
|
|
94
|
+
const current = !s.done && plan.findIndex((x) => !x.done) === i;
|
|
95
|
+
return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || current ? '$ink' : '$soft', children: s.done ? '✓' : current ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
|
|
96
|
+
})] })) : null, running ? (_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", children: "Environment setup takes several minutes." }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), onStop: () => void halt(), busy: running && !draft.trim(), disabled: !running || busy, placeholder: running ? 'Add a follow up' : 'This run has finished', label: "Steer this run" })] })] }), desk ? _jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: record?.mode ?? '', pr: pr, events: events, live: running, refused: refused || (detail.error ? detail.error.message : ''), onRetry: detail.reload, onHide: () => setDesk(false) }) : null] }));
|
|
67
97
|
}
|
package/lib/section.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export declare const DOTS: Record<string, SessionStatus>;
|
|
|
8
8
|
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
9
9
|
export declare function useSessions(host: Host): Read<Session[]>;
|
|
10
10
|
/**
|
|
11
|
-
* New,
|
|
12
|
-
* and the runs — where a host lists its
|
|
13
|
-
* so a host that draws its rail as a drawer
|
|
11
|
+
* New, Automations, the forge (Codebase, Projects, Issues), Artifacts, Templates, then the
|
|
12
|
+
* runs. `children` sit between the places and the runs — where a host lists its
|
|
13
|
+
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
14
|
+
* on a phone can close it.
|
|
14
15
|
*/
|
|
15
16
|
export declare function DevSection({ host, onPick, label, children, }: {
|
|
16
17
|
host: Host;
|
package/lib/section.js
CHANGED
|
@@ -11,9 +11,10 @@ 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, LayoutTemplate, SquarePen } from '@hanzogui/lucide-icons-2';
|
|
14
|
+
import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
15
15
|
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
|
+
import { pinBoard } from './choice.js';
|
|
17
18
|
import { useRecents } from './data.js';
|
|
18
19
|
import { path, route } from './route.js';
|
|
19
20
|
/** A run's status, as the rail's dot draws it. Anything else is idle. */
|
|
@@ -31,9 +32,10 @@ export function useSessions(host) {
|
|
|
31
32
|
}
|
|
32
33
|
const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1", children: children }));
|
|
33
34
|
/**
|
|
34
|
-
* New,
|
|
35
|
-
* and the runs — where a host lists its
|
|
36
|
-
* so a host that draws its rail as a drawer
|
|
35
|
+
* New, Automations, the forge (Codebase, Projects, Issues), Artifacts, Templates, then the
|
|
36
|
+
* runs. `children` sit between the places and the runs — where a host lists its
|
|
37
|
+
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
38
|
+
* on a phone can close it.
|
|
37
39
|
*/
|
|
38
40
|
export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
39
41
|
const sessions = useSessions(host);
|
|
@@ -43,5 +45,8 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
|
43
45
|
onPick?.();
|
|
44
46
|
};
|
|
45
47
|
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(
|
|
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
|
+
pinBoard(host.org, '');
|
|
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.' })) })] }));
|
|
47
52
|
}
|
package/lib/sync.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Sync(): import("react").JSX.Element;
|
package/lib/sync.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Bring repositories the GitHub connection already grants onto the forge.
|
|
4
|
+
*
|
|
5
|
+
* The list is GET /v1/provider/github/repos. The queue is POST
|
|
6
|
+
* /v1/provider/github/repos/import, which mirrors `owner/name` into this
|
|
7
|
+
* organization's forge in the background. An account named unread and holding
|
|
8
|
+
* no repositories is shown as needing an organization admin.
|
|
9
|
+
*/
|
|
10
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
+
import { Check, ChevronLeft, ChevronRight, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
12
|
+
import { Button, Input } from '@hanzo/ui';
|
|
13
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { accounts, bring, connect, grants } from './api/github.js';
|
|
15
|
+
import { readPending, writePending } from './choice.js';
|
|
16
|
+
import { useRead } from './data.js';
|
|
17
|
+
import { useHost, useTarget } from './host.js';
|
|
18
|
+
import { path } from './route.js';
|
|
19
|
+
const EMPTY = { repos: [], unread: [] };
|
|
20
|
+
function noteOf(e, fallback) {
|
|
21
|
+
return e instanceof Error ? e.message : fallback;
|
|
22
|
+
}
|
|
23
|
+
export function Sync() {
|
|
24
|
+
const host = useHost();
|
|
25
|
+
const t = useTarget();
|
|
26
|
+
const signed = Boolean(host.person);
|
|
27
|
+
const [q, setQ] = useState('');
|
|
28
|
+
const [owner, setOwner] = useState('');
|
|
29
|
+
const [step, setStep] = useState('pick');
|
|
30
|
+
const [picked, setPicked] = useState([]);
|
|
31
|
+
const [busy, setBusy] = useState(false);
|
|
32
|
+
const [syncing, setSyncing] = useState(false);
|
|
33
|
+
const [error, setError] = useState('');
|
|
34
|
+
const list = useRead(signed ? () => grants(t) : null, EMPTY, [t, signed]);
|
|
35
|
+
const rows = list.value.repos;
|
|
36
|
+
const orgs = useMemo(() => accounts(list.value), [list.value]);
|
|
37
|
+
const needle = q.trim().toLowerCase();
|
|
38
|
+
const shownOrgs = orgs.filter((a) => !needle || a.name.toLowerCase().includes(needle));
|
|
39
|
+
const ofOwner = rows.filter((r) => r.owner === owner);
|
|
40
|
+
const shownRepos = ofOwner.filter((r) => !needle || r.name.toLowerCase().includes(needle) || r.fullName.toLowerCase().includes(needle));
|
|
41
|
+
const chosen = rows.filter((r) => picked.includes(r.fullName));
|
|
42
|
+
const lead = chosen[0]?.name ?? '';
|
|
43
|
+
const more = Math.max(0, chosen.length - 1);
|
|
44
|
+
const dest = host.org || 'Forge';
|
|
45
|
+
const toggle = (fullName) => {
|
|
46
|
+
setPicked((cur) => (cur.includes(fullName) ? cur.filter((n) => n !== fullName) : [...cur, fullName]));
|
|
47
|
+
};
|
|
48
|
+
const selectShown = () => {
|
|
49
|
+
const names = shownRepos.map((r) => r.fullName);
|
|
50
|
+
const all = names.every((n) => picked.includes(n));
|
|
51
|
+
setPicked((cur) => (all ? cur.filter((n) => !names.includes(n)) : [...new Set([...cur, ...names])]));
|
|
52
|
+
};
|
|
53
|
+
const addOrg = async () => {
|
|
54
|
+
setError('');
|
|
55
|
+
try {
|
|
56
|
+
const url = await connect(t);
|
|
57
|
+
host.open(url);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
setError(noteOf(e, 'Could not open the GitHub connection'));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const send = async () => {
|
|
64
|
+
setBusy(true);
|
|
65
|
+
setError('');
|
|
66
|
+
try {
|
|
67
|
+
await bring(t, picked);
|
|
68
|
+
const next = [
|
|
69
|
+
...readPending(host.org).filter((p) => !picked.includes(p.fullName)),
|
|
70
|
+
...chosen.map((r) => ({ fullName: r.fullName, name: r.name })),
|
|
71
|
+
];
|
|
72
|
+
writePending(host.org, next);
|
|
73
|
+
setSyncing(true);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
setError(noteOf(e, 'Could not queue those repositories'));
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
setBusy(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const back = () => {
|
|
83
|
+
host.go(path({ kind: 'screen', screen: 'codebases' }));
|
|
84
|
+
};
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!syncing)
|
|
87
|
+
return;
|
|
88
|
+
let stop = false;
|
|
89
|
+
const tick = async () => {
|
|
90
|
+
try {
|
|
91
|
+
const g = await grants(t);
|
|
92
|
+
const landed = new Set(g.repos.filter((r) => r.imported).map((r) => r.fullName));
|
|
93
|
+
const left = readPending(host.org).filter((p) => !landed.has(p.fullName));
|
|
94
|
+
writePending(host.org, left);
|
|
95
|
+
if (!stop && picked.every((n) => landed.has(n)))
|
|
96
|
+
back();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* the button stays on Syncing until the next look */
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const id = window.setInterval(() => void tick(), 4000);
|
|
103
|
+
return () => {
|
|
104
|
+
stop = true;
|
|
105
|
+
window.clearInterval(id);
|
|
106
|
+
};
|
|
107
|
+
}, [syncing, t, host.org, host.go, picked]);
|
|
108
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to codebases", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Codebase" }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: "/" }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Sync" })] }), _jsxs(YStack, { flex: 1, items: "center", justify: "center", px: "$6", pb: "$10", gap: "$5", children: [_jsxs(XStack, { justify: "center", gap: "$6", children: [_jsx(Step, { label: "Select repositories", done: step === 'access', current: step === 'pick' }), _jsx(Step, { label: "Repository access", done: false, current: step === 'access' })] }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$5", bg: "$panel", px: "$6", py: "$6", gap: "$4", width: "100%", maxW: 460, children: [step === 'pick' && !owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search organizations", "aria-label": "Search organizations", disabled: !signed }), signed ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${orgs.length} ${orgs.length === 1 ? 'organization' : 'organizations'}` })) : null, list.error ? _jsx(Soft, { children: list.error.message }) : null, !signed ? (_jsx(Soft, { children: "Sign in to choose repositories the GitHub connection can read." })) : list.loading && rows.length === 0 && orgs.length === 0 ? (_jsx(Soft, { children: "Reading the connection\u2026" })) : shownOrgs.length === 0 ? (_jsx(Soft, { children: orgs.length === 0 ? 'No organizations on this connection yet.' : 'Nothing matches.' })) : (shownOrgs.map((a) => (_jsxs(XStack, { render: "button", "aria-label": a.blocked ? `${a.name} needs an organization admin` : `Open ${a.name}`, disabled: a.blocked, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: () => {
|
|
109
|
+
setOwner(a.name);
|
|
110
|
+
setQ('');
|
|
111
|
+
}, children: [_jsx(SizableText, { flex: 1, size: "$2", color: a.blocked ? '$soft' : '$ink', numberOfLines: 1, children: a.name }), a.blocked ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Needs organization admin" })) : (_jsx(ChevronRight, { size: 14 }))] }, a.name)))), _jsxs(XStack, { render: "button", "aria-label": "Add organization", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), disabled: !signed, children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add organization" })] })] })] })) : null, step === 'pick' && owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { render: "button", "aria-label": "All organizations", onPress: () => setOwner(''), children: _jsx(ChevronLeft, { size: 16 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: owner })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search repositories", "aria-label": "Search repositories" }), _jsxs(XStack, { justify: "space-between", items: "center", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [ofOwner.length, " ", ofOwner.length === 1 ? 'repository' : 'repositories'] }), _jsx(XStack, { render: "button", "aria-label": "Select all", onPress: selectShown, disabled: shownRepos.length === 0, children: _jsx(SizableText, { size: "$1", color: "$ink", children: "Select all" }) })] }), shownRepos.length === 0 ? (_jsx(Soft, { children: "Nothing matches." })) : (shownRepos.map((r) => (_jsx(RepoRow, { repo: r, on: picked.includes(r.fullName), onPress: () => toggle(r.fullName) }, r.fullName)))), _jsxs(XStack, { render: "button", "aria-label": "Grant more repositories", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add repositories" })] })] })] })) : null, step === 'access' ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Repository access" }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: "Review who can read these repositories" })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsxs(XStack, { flex: 1, items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Clear the selection", onPress: () => { setPicked([]); setStep('pick'); }, children: _jsx(X, { size: 12 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: more ? `${lead}, +${more} more` : lead })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setStep('pick'), children: "Edit" })] }), _jsxs(XStack, { justify: "space-between", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Who gets access" }), _jsxs(SizableText, { size: "$2", color: "$ink", children: [host.person?.name || host.person?.email || 'You', " \u00B7 You"] })] }), _jsxs(YStack, { gap: "$1", items: "flex-end", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Role" }), _jsx(SizableText, { size: "$2", color: "$ink", children: host.admin ? 'Repository admin' : 'Member' })] })] }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Anyone with access to ", dest] })] })) : null, error ? _jsx(Soft, { children: error }) : null, step === 'pick' ? (_jsx(Button, { width: "100%", disabled: !owner || picked.length === 0, onPress: () => setStep('access'), children: "Continue" })) : (_jsxs(YStack, { gap: "$3", items: "center", width: "100%", children: [_jsx(Button, { width: "100%", disabled: busy || syncing || picked.length === 0, onPress: () => void send(), children: syncing || busy ? 'Syncing…' : `Sync ${picked.length} ${picked.length === 1 ? 'repository' : 'repositories'} to ${dest}` }), syncing ? null : (_jsx(XStack, { render: "button", "aria-label": "Sync later", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Sync later" }) }))] })), !signed ? (_jsx(Button, { width: "100%", onPress: () => host.signIn?.(), children: "Sign in" })) : null] })] })] }));
|
|
112
|
+
}
|
|
113
|
+
function Step({ label, done, current }) {
|
|
114
|
+
return (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(YStack, { width: 14, height: 14, rounded: 999, bg: done ? '$green10' : 'transparent', borderWidth: 1, borderColor: done || current ? '$ink' : '$borderColor', items: "center", justify: "center", children: done ? _jsx(Check, { size: 10, color: "white" }) : null }), _jsx(SizableText, { size: "$1", color: current || done ? '$ink' : '$soft', children: label })] }));
|
|
115
|
+
}
|
|
116
|
+
function Panel({ children }) {
|
|
117
|
+
return (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", px: "$3", py: "$2", gap: "$2", maxH: 360, overflow: "scroll", children: children }));
|
|
118
|
+
}
|
|
119
|
+
function Soft({ children }) {
|
|
120
|
+
return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
|
|
121
|
+
}
|
|
122
|
+
function RepoRow({ repo, on, onPress }) {
|
|
123
|
+
return (_jsxs(XStack, { render: "button", "aria-label": on ? `Selected ${repo.name}` : `Select ${repo.name}`, "aria-pressed": on, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: onPress, children: [_jsx(YStack, { width: 14, height: 14, rounded: "$1", borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', bg: on ? '$ink' : 'transparent' }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: repo.name }), repo.imported ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "On Forge" })) : null] }));
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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).",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"build:lib": "rm -rf lib && tsc -p tsconfig.lib.json",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
67
|
"preview": "vite preview",
|
|
68
|
-
"test": "vitest run"
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"e2e": "HANZO_E2E=1 vitest run src/e2e"
|
|
69
70
|
}
|
|
70
71
|
}
|