@hanzo/build 0.2.0

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.
Files changed (57) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/NOTICE +37 -0
  4. package/README.md +120 -0
  5. package/lib/account.d.ts +11 -0
  6. package/lib/account.js +26 -0
  7. package/lib/api/call.d.ts +38 -0
  8. package/lib/api/call.js +75 -0
  9. package/lib/api/coding.d.ts +56 -0
  10. package/lib/api/coding.js +58 -0
  11. package/lib/api/git.d.ts +27 -0
  12. package/lib/api/git.js +39 -0
  13. package/lib/api/github.d.ts +78 -0
  14. package/lib/api/github.js +73 -0
  15. package/lib/api/models.d.ts +17 -0
  16. package/lib/api/models.js +28 -0
  17. package/lib/api/places.d.ts +25 -0
  18. package/lib/api/places.js +31 -0
  19. package/lib/api/platform.d.ts +59 -0
  20. package/lib/api/platform.js +69 -0
  21. package/lib/api/projects.d.ts +52 -0
  22. package/lib/api/projects.js +97 -0
  23. package/lib/api/sessions.d.ts +74 -0
  24. package/lib/api/sessions.js +120 -0
  25. package/lib/api/sse.d.ts +24 -0
  26. package/lib/api/sse.js +68 -0
  27. package/lib/api/turn.d.ts +51 -0
  28. package/lib/api/turn.js +121 -0
  29. package/lib/api/verdict.d.ts +12 -0
  30. package/lib/api/verdict.js +20 -0
  31. package/lib/builder.d.ts +5 -0
  32. package/lib/builder.js +81 -0
  33. package/lib/data.d.ts +42 -0
  34. package/lib/data.js +178 -0
  35. package/lib/host.d.ts +57 -0
  36. package/lib/host.js +26 -0
  37. package/lib/index.d.ts +11 -0
  38. package/lib/index.js +10 -0
  39. package/lib/landing.d.ts +13 -0
  40. package/lib/landing.js +132 -0
  41. package/lib/out.d.ts +10 -0
  42. package/lib/out.js +4 -0
  43. package/lib/project.d.ts +3 -0
  44. package/lib/project.js +250 -0
  45. package/lib/publish.d.ts +15 -0
  46. package/lib/publish.js +94 -0
  47. package/lib/route.d.ts +30 -0
  48. package/lib/route.js +28 -0
  49. package/lib/run.d.ts +3 -0
  50. package/lib/run.js +67 -0
  51. package/lib/shelf.d.ts +2 -0
  52. package/lib/shelf.js +60 -0
  53. package/lib/tools.d.ts +25 -0
  54. package/lib/tools.js +64 -0
  55. package/lib/voice.d.ts +17 -0
  56. package/lib/voice.js +86 -0
  57. package/package.json +70 -0
package/lib/project.js ADDED
@@ -0,0 +1,250 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A project's workspace, in the same window: the chat on the left, what it
4
+ * built on the right.
5
+ *
6
+ * bar mark · project · history · chat toggle | Preview Files Code Layers ·
7
+ * device · reload · page · open | Share · Publish
8
+ * left the project's runs as a conversation, suggestions, and the ask
9
+ * right the deployed page, framed; the repository at the run's branch as a
10
+ * tree and as read-only code; the element a person picked
11
+ * dock the run's console — its steps and its log, as they stream
12
+ *
13
+ * A chat turn IS a coding run: `POST /v1/agent/coding` with the project, the
14
+ * mode, and the previous run as `after`, so the next ask builds on the branch
15
+ * the last one pushed. Files and Code read native git at that branch. The
16
+ * preview is the project's own deployed address; a run's sandbox serves no
17
+ * address the platform publishes, and nothing here invents one.
18
+ *
19
+ * Derived from the Hanzo App v2 editor (github.com/hanzoai/build-v2), itself
20
+ * derived from OSW Studio and DeepSite (MIT). See NOTICE.
21
+ */
22
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
23
+ import { Clock, ExternalLink, MousePointerClick, PanelLeft, RefreshCcw, Share2, Upload, } from '@hanzogui/lucide-icons-2';
24
+ import { Attachments, Console, DEVICES, Feedback, FileTabs, FileTree, ModeSelect, PageSelect, PreviewFrame, ProjectChip, Suggestions, SUGGESTIONS, VIEWS, Views, Workspace, fold, } from '@hanzo/ui/agents';
25
+ import { Composer, Message } from '@hanzo/ui/chat';
26
+ import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
27
+ import { HanzoMark } from '@hanzo/ui/product';
28
+ import { useEffect, useMemo, useRef, useState } from 'react';
29
+ import { start, unhonoured } from './api/coding.js';
30
+ import { blob, tree } from './api/git.js';
31
+ import { name as repoName, ours } from './api/projects.js';
32
+ import { list, message, stop } from './api/sessions.js';
33
+ import { outcome, pull, said, who } from './api/turn.js';
34
+ import { verdict as record } from './api/verdict.js';
35
+ import { useKept, useProjects, useRead, useRun } from './data.js';
36
+ import { useHost, useTarget } from './host.js';
37
+ import { MODES } from './landing.js';
38
+ import { Out } from './out.js';
39
+ import { Publish } from './publish.js';
40
+ import { Attach, compose, Dictate, Files } from './tools.js';
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
+ /** One run in the conversation: what was asked, and what it came to. */
48
+ function Turn({ run, open, onOpen, project }) {
49
+ const t = useTarget();
50
+ const [v, setV] = useState(null);
51
+ const [note, setNote] = useState('');
52
+ return (_jsxs(YStack, { gap: "$2", children: [_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$2", color: "$ink", children: run.title || 'Untitled run' }) }), _jsx(Message, { role: "assistant", busy: LIVE.has(run.status), actions: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Feedback, { text: run.title, verdict: v, onVerdict: (next) => {
53
+ setV(next);
54
+ record(t, run.id, project, next).catch((e) => {
55
+ setV(null);
56
+ setNote(e instanceof Error ? e.message : 'Not recorded');
57
+ });
58
+ } }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: note })) : null, !open ? (_jsx(XStack, { render: "button", onPress: onOpen, hitSlop: 8, children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Show steps" }) })) : null] }), children: _jsx(SizableText, { size: "$2", color: "$ink", children: run.status === 'done' ? 'Done.' : run.status === 'error' ? 'This run hit an error.' : run.status === 'stopped' ? 'Stopped.' : 'Working…' }) })] }));
59
+ }
60
+ export function Project({ slug }) {
61
+ const host = useHost();
62
+ const t = useTarget();
63
+ const signed = Boolean(host.person);
64
+ const all = useProjects(t, signed);
65
+ const project = all.value.find((p) => p.slug === slug) ?? null;
66
+ const listed = useRead(signed ? () => list(t, { kind: 'coding', project: slug, limit: 50 }) : null, [], [t, signed, slug]);
67
+ // Only the runs on this project's own repository: a record can be moved into
68
+ // any project, and a run's branch is what Files, Code and Publish read.
69
+ const runs = { ...listed, value: listed.value.filter((r) => ours(r.repo, project?.repo ?? '')) };
70
+ const ordered = useMemo(() => [...runs.value].reverse(), [listed.value, project?.repo]);
71
+ const [chosen, setChosen] = useState(null);
72
+ const current = chosen ?? runs.value[0]?.id ?? null;
73
+ const run = useRun(t, current);
74
+ const end = outcome(run.events);
75
+ const pr = pull(run.record?.pr ?? '', run.record?.repo ?? '');
76
+ const running = LIVE.has(run.status || end.status);
77
+ const [view, setView] = useKept(`hanzo.build.view.${slug}`, 'preview');
78
+ const [device, setDevice] = useState('desktop');
79
+ const [collapsed, setCollapsed] = useKept('hanzo.build.chat', false);
80
+ const [pane, setPane] = useState('chat');
81
+ const [dock, setDock] = useKept('hanzo.build.dock', 36);
82
+ const [dismissed, setDismissed] = useKept(`hanzo.build.hints.${slug}`, false);
83
+ const [mode, setMode] = useKept('hanzo.build.mode', 'build');
84
+ const [page, setPage] = useState('/');
85
+ const [draft, setDraft] = useState('');
86
+ const [busy, setBusy] = useState(false);
87
+ const [note, setNote] = useState('');
88
+ const [picked, setPicked] = useState([]);
89
+ const [attached, setAttached] = useState([]);
90
+ const [bridge, setBridge] = useState(false);
91
+ const [editing, setEditing] = useState(false);
92
+ const [pageLines, setPageLines] = useState([]);
93
+ const [history, setHistory] = useState(false);
94
+ const [publish, setPublish] = useState(null);
95
+ const [switcher, setSwitcher] = useState(false);
96
+ const frame = useRef(null);
97
+ // Files and Code: native git, at the branch the open run pushed (or the project's own).
98
+ const repo = project?.repo ? repoName(project.repo) : '';
99
+ const ref = run.record?.branch || project?.branch || 'main';
100
+ const [files, setFiles] = useState([]);
101
+ const [file, setFile] = useState(null);
102
+ useEffect(() => {
103
+ setFiles([]);
104
+ setFile(null);
105
+ }, [repo, ref]);
106
+ const openFile = async (path) => {
107
+ setFile(path);
108
+ setView('code');
109
+ if (files.some((f) => f.path === path))
110
+ return;
111
+ setFiles((was) => [...was, { path, readOnly: true }]);
112
+ try {
113
+ const b = await blob(t, repo, ref, path);
114
+ const content = b.binary ? '' : b.truncated ? '' : b.text;
115
+ const error = b.binary ? 'A binary file — nothing to show as text.' : b.truncated ? 'Past the 1 MiB view cap — clone the repository to read it.' : undefined;
116
+ setFiles((was) => was.map((f) => (f.path === path ? { ...f, content, error } : f)));
117
+ }
118
+ catch (e) {
119
+ setFiles((was) => was.map((f) => (f.path === path ? { ...f, error: e instanceof Error ? e.message : 'Could not read this file' } : f)));
120
+ }
121
+ };
122
+ // Pages: the HTML files at the repository's root, as the page picker's list.
123
+ const root = useRead(repo ? () => tree(t, repo, ref, '') : null, [], [t, repo, ref]);
124
+ const pages = useMemo(() => [
125
+ { id: '/', label: 'Homepage' },
126
+ ...root.value
127
+ .filter((e) => !e.dir && /\.html?$/i.test(e.name) && !/^index\.html?$/i.test(e.name))
128
+ .map((e) => ({ id: `/${e.path}`, label: e.name.replace(/\.html?$/i, '') })),
129
+ ], [root.value]);
130
+ const src = project?.live ? new URL(page, project.live).toString() : null;
131
+ const lines = useMemo(() => [
132
+ ...run.events
133
+ .filter((e) => e.kind === 'log' || e.kind === 'tool-call' || e.kind === 'status')
134
+ .map((e) => ({
135
+ 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),
138
+ source: e.kind === 'status' ? 'run' : who(e.actor),
139
+ }))
140
+ .filter((l) => l.text),
141
+ ...pageLines,
142
+ ], [run.events, pageLines]);
143
+ const onBridge = (e) => {
144
+ if (e.type === 'preview:ready') {
145
+ setBridge(true);
146
+ // A new document starts closed to picking; open it again if the person had.
147
+ if (editing)
148
+ frame.current?.post({ type: 'preview:editable', active: true });
149
+ }
150
+ else if (e.type === 'preview:select')
151
+ // The whole selector, as it will ride the next ask — never a shortened name
152
+ // that hides what the page sent.
153
+ setPicked([{ id: e.info.selector, kind: 'element', label: e.info.selector }]);
154
+ else if (e.type === 'preview:navigate' && project?.live) {
155
+ // Resolved on the live site's own origin, and kept as its path and query.
156
+ const to = new URL(e.path, project.live);
157
+ if (to.origin === new URL(project.live).origin)
158
+ setPage(`${to.pathname}${to.search}`);
159
+ }
160
+ else if (e.type === 'preview:console')
161
+ setPageLines((was) => [...was.slice(-400), { id: `p${was.length}-${Date.now()}`, level: e.level, text: e.text, source: 'page' }]);
162
+ };
163
+ const send = async (text) => {
164
+ const ask = (text ?? draft).trim();
165
+ if (!ask || busy)
166
+ return;
167
+ if (!signed)
168
+ return host.signIn?.();
169
+ setBusy(true);
170
+ setNote('');
171
+ try {
172
+ // The page's own strings ride as quoted data, never as prose, and the page
173
+ // is named only when the person chose it from the picker.
174
+ const where = pages.find((p) => p.id === page)?.label;
175
+ const context = picked.length
176
+ ? `\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
+ : '';
178
+ const gh = project ? github(project.repo) : '';
179
+ const next = await start(t, {
180
+ prompt: compose(`${ask}${context}`, attached),
181
+ project: slug,
182
+ repo: gh || undefined,
183
+ base: gh ? project?.branch || undefined : undefined,
184
+ after: current ?? undefined,
185
+ mode,
186
+ });
187
+ setDraft('');
188
+ setPicked([]);
189
+ setAttached([]);
190
+ setChosen(next.session);
191
+ runs.reload();
192
+ }
193
+ catch (e) {
194
+ setNote(e instanceof Error ? e.message : 'The run could not start');
195
+ }
196
+ finally {
197
+ setBusy(false);
198
+ }
199
+ };
200
+ const steer = async () => {
201
+ if (!current || !draft.trim())
202
+ return;
203
+ try {
204
+ await message(t, current, draft);
205
+ setDraft('');
206
+ setNote('Sent — the run reads it before its next step');
207
+ }
208
+ catch (e) {
209
+ setNote(e instanceof Error ? e.message : 'Could not reach this run');
210
+ }
211
+ };
212
+ const share = async () => {
213
+ const link = project?.live || `${window.location.origin}${window.location.pathname}`;
214
+ try {
215
+ await navigator.clipboard.writeText(link);
216
+ setNote(`Copied ${link}`);
217
+ }
218
+ catch {
219
+ setNote(link);
220
+ }
221
+ };
222
+ if (!signed) {
223
+ return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", children: [_jsx(SizableText, { size: "$4", color: "$ink", children: "Sign in to open this project." }), _jsx(Button, { onPress: () => host.signIn?.(), children: "Sign in" })] }));
224
+ }
225
+ if (!all.loading && !project && !all.error) {
226
+ return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", children: host.org ? `${host.org} has no project named ${slug}.` : `There is no project named ${slug}.` }), _jsx(Button, { variant: "outline", onPress: () => host.go('-/artifacts'), children: "See what this organization has built" })] }));
227
+ }
228
+ 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
+ ? '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 })] })] })] }));
231
+ 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
+ const next = !editing;
233
+ setEditing(next);
234
+ frame.current?.post({ type: 'preview:editable', active: next });
235
+ }, p: "$2", rounded: "$10", children: _jsx(MousePointerClick, { size: 16 }) })) : undefined })) : view === 'files' ? (repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` })) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
236
+ setFiles((was) => was.filter((f) => f.path !== p));
237
+ if (file === p)
238
+ setFile(null);
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
+ ? 'Pick an element in the preview to see it here and attach it to your next ask.'
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 &&
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
+ setChosen(r.id);
245
+ setHistory(false);
246
+ }, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children: all.value.map((p) => (_jsx(XStack, { render: "button", onPress: () => {
247
+ setSwitcher(false);
248
+ host.go(p.slug);
249
+ }, px: "$2", py: "$1.5", rounded: "$3", bg: p.slug === slug ? '$hover' : undefined, hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: p.name }) }, p.slug))) })] }) }), _jsx(Publish, { source: publish, onClose: () => setPublish(null) })] }));
250
+ }
@@ -0,0 +1,15 @@
1
+ export interface Source {
2
+ /** The https clone URL. */
3
+ repo: string;
4
+ /** What to call it: `owner/name` or a project's name. */
5
+ title: string;
6
+ ref: string;
7
+ /** The app name to declare. */
8
+ name: string;
9
+ /** The project to add it to, when there already is one. */
10
+ project?: string;
11
+ }
12
+ export declare function Publish({ source, onClose }: {
13
+ source: Source | null;
14
+ onClose: () => void;
15
+ }): import("react").JSX.Element | null;
package/lib/publish.js ADDED
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Adding a repository to a project — the one publish path, from a repo row
4
+ * ("Add to project") and from a workspace's Publish.
5
+ *
6
+ * `POST /v1/platform/apps` builds the repository and writes its declaration.
7
+ * On a branch (everyone) that is a review: nothing deploys until it merges,
8
+ * and the review link is the thing to open. An org admin can take a green
9
+ * build to main, which CD applies: that is the second call, with the build's
10
+ * tag, because main refuses an image that is not built yet.
11
+ *
12
+ * Every state shown is one the platform reported; a refusal shows the
13
+ * platform's own sentence.
14
+ */
15
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
16
+ import { ExternalLink } from '@hanzogui/lucide-icons-2';
17
+ import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, Input } from '@hanzo/ui';
18
+ import { useEffect, useState } from 'react';
19
+ import { builds, declare, label } from './api/platform.js';
20
+ import { useHost, useTarget } from './host.js';
21
+ import { Out } from './out.js';
22
+ export function Publish({ source, onClose }) {
23
+ const t = useTarget();
24
+ const host = useHost();
25
+ const [project, setProject] = useState('');
26
+ const [phase, setPhase] = useState('ask');
27
+ const [out, setOut] = useState(null);
28
+ const [status, setStatus] = useState('');
29
+ const [error, setError] = useState('');
30
+ useEffect(() => {
31
+ setProject(source?.project ?? source?.name ?? '');
32
+ setPhase('ask');
33
+ setOut(null);
34
+ setStatus('');
35
+ setError('');
36
+ }, [source]);
37
+ // The build is asynchronous; its record says when it is green.
38
+ useEffect(() => {
39
+ const id = out?.build?.id;
40
+ if (!id || phase !== 'declared')
41
+ return;
42
+ let live = true;
43
+ const tick = async () => {
44
+ try {
45
+ const row = (await builds(t)).find((b) => b.id === id);
46
+ if (live && row)
47
+ setStatus(row.status);
48
+ }
49
+ catch {
50
+ /* the status stays as last read */
51
+ }
52
+ };
53
+ void tick();
54
+ const every = setInterval(tick, 5000);
55
+ return () => {
56
+ live = false;
57
+ clearInterval(every);
58
+ };
59
+ }, [t, out, phase]);
60
+ if (!source)
61
+ return null;
62
+ const send = async () => {
63
+ setPhase('sending');
64
+ setError('');
65
+ try {
66
+ const d = await declare(t, { repo: source.repo, ref: source.ref, name: source.name, project: project || source.name, mode: 'branch' });
67
+ setOut(d);
68
+ setStatus(d.build?.status ?? '');
69
+ setPhase('declared');
70
+ }
71
+ catch (e) {
72
+ setError(e instanceof Error ? e.message : 'The platform refused this');
73
+ setPhase('failed');
74
+ }
75
+ };
76
+ const ship = async () => {
77
+ if (!out?.build?.id)
78
+ return;
79
+ setPhase('shipping');
80
+ setError('');
81
+ try {
82
+ await declare(t, { repo: source.repo, ref: source.ref, name: source.name, project: project || source.name, mode: 'commit', tag: out.build.id });
83
+ setPhase('shipped');
84
+ }
85
+ catch (e) {
86
+ setError(e instanceof Error ? e.message : 'The platform refused this');
87
+ setPhase('declared');
88
+ }
89
+ };
90
+ const green = /^(succeeded|success|done|complete|completed|ok)$/i.test(status);
91
+ return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Add to project" }), _jsxs(DialogDescription, { children: ["Build ", source.title, " at ", source.ref || 'main', " and declare it in a project. ", host.admin ? 'As an admin you can take a green build to main.' : 'It opens a review; merging it is what deploys.'] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { render: "label", size: "$2", color: "$soft", htmlFor: "publish-project", children: "Project" }), _jsx(Input, { id: "publish-project", value: project, onChangeText: (v) => setProject(v), disabled: phase !== 'ask' && phase !== 'failed', placeholder: label(source.name), "aria-label": "Project" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Saved as ", label(project || source.name)] })] }), out ? (_jsxs(YStack, { gap: "$2", role: "status", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: phase === 'shipped'
92
+ ? 'Declared on main — CD applies it on its next pass.'
93
+ : `Build ${out.build?.id ?? ''}: ${status || 'building'}` }), out.review ? (_jsxs(Out, { href: out.review, children: [_jsx(SizableText, { size: "$2", color: "$ink", textDecorationLine: "underline", children: "Open the review" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] })) : null] })) : null, error ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: error })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { variant: "outline", size: "sm", onPress: onClose, children: phase === 'shipped' || phase === 'declared' ? 'Done' : 'Cancel' }), phase === 'ask' || phase === 'failed' || phase === 'sending' ? (_jsx(Button, { size: "sm", disabled: phase === 'sending', onPress: () => void send(), children: phase === 'sending' ? 'Adding…' : 'Add to project' })) : host.admin && (phase === 'declared' || phase === 'shipping') ? (_jsx(Button, { size: "sm", disabled: !green || phase === 'shipping', onPress: () => void ship(), children: phase === 'shipping' ? 'Shipping…' : green ? 'Ship to main' : 'Waiting for the build' })) : null] })] }) }));
94
+ }
package/lib/route.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The builder's addresses, under whatever path the host mounts it at.
3
+ *
4
+ * '' the empty state: a new run
5
+ * sess_<32 hex> one run
6
+ * -/<screen> a builder screen (artifacts, templates)
7
+ * <slug> a project's workspace
8
+ *
9
+ * Unambiguous by construction: a project slug is `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`,
10
+ * so it can hold neither the `_` a session id carries nor start with `-`.
11
+ * Anything else is not an address here and reads as the empty state.
12
+ */
13
+ export type Screen = 'artifacts' | 'templates';
14
+ export type Route = {
15
+ kind: 'new';
16
+ } | {
17
+ kind: 'run';
18
+ id: string;
19
+ } | {
20
+ kind: 'screen';
21
+ screen: Screen;
22
+ } | {
23
+ kind: 'project';
24
+ slug: string;
25
+ };
26
+ export declare const SESSION: RegExp;
27
+ export declare const SLUG: RegExp;
28
+ export declare function route(path: string): Route;
29
+ /** The path for a route — the inverse of `route`. */
30
+ export declare function path(r: Route): string;
package/lib/route.js ADDED
@@ -0,0 +1,28 @@
1
+ export const SESSION = /^sess_[0-9a-f]{32}$/;
2
+ export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
3
+ const SCREENS = ['artifacts', 'templates'];
4
+ export function route(path) {
5
+ const p = path.replace(/^\/+|\/+$/g, '');
6
+ if (SESSION.test(p))
7
+ return { kind: 'run', id: p };
8
+ if (p.startsWith('-/')) {
9
+ const s = p.slice(2);
10
+ return SCREENS.includes(s) ? { kind: 'screen', screen: s } : { kind: 'new' };
11
+ }
12
+ if (SLUG.test(p))
13
+ return { kind: 'project', slug: p };
14
+ return { kind: 'new' };
15
+ }
16
+ /** The path for a route — the inverse of `route`. */
17
+ export function path(r) {
18
+ switch (r.kind) {
19
+ case 'new':
20
+ return '';
21
+ case 'run':
22
+ return r.id;
23
+ case 'screen':
24
+ return `-/${r.screen}`;
25
+ case 'project':
26
+ return r.slug;
27
+ }
28
+ }
package/lib/run.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function Run({ id }: {
2
+ id: string;
3
+ }): import("react").JSX.Element;
package/lib/run.js ADDED
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * One run, live: its transcript as it streams, the way to steer or stop it,
4
+ * and the pull request once it pushes one.
5
+ *
6
+ * The detail read is the record and the stream adds to it; the two overlap
7
+ * whenever a turn lands between the read and the subscribe, so turns are merged
8
+ * by sequence. A reconnect after a drop re-reads the detail — the platform drops
9
+ * a subscriber that falls behind, and frames missed then are only in the read.
10
+ *
11
+ * Steering is a queue the run drains between steps: a press is confirmed as
12
+ * RECORDED, and the status is left to the feed to correct.
13
+ */
14
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
15
+ import { ExternalLink, GitPullRequest } from '@hanzogui/lucide-icons-2';
16
+ import { Transcript, fold } from '@hanzo/ui/agents';
17
+ import { Composer } from '@hanzo/ui/chat';
18
+ import { useMemo, useState } from 'react';
19
+ import { message, stop } from './api/sessions.js';
20
+ import { outcome, pull, said, who } from './api/turn.js';
21
+ import { useRun } from './data.js';
22
+ import { useTarget } from './host.js';
23
+ import { Out } from './out.js';
24
+ const LIVE = new Set(['running', 'paused', '']);
25
+ export function Run({ id }) {
26
+ const t = useTarget();
27
+ const { detail, record, events, status, refused } = useRun(t, id);
28
+ const [draft, setDraft] = useState('');
29
+ const [note, setNote] = useState('');
30
+ const [busy, setBusy] = useState(false);
31
+ 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]);
34
+ const end = outcome(events);
35
+ const state = status || end.status;
36
+ const pr = pull(record?.pr ?? '', record?.repo ?? '');
37
+ const running = LIVE.has(state) && !detail.error;
38
+ const send = async () => {
39
+ if (!draft.trim() || busy)
40
+ return;
41
+ setBusy(true);
42
+ setNote('');
43
+ try {
44
+ await message(t, id, draft);
45
+ setDraft('');
46
+ setNote('Sent — the run reads it before its next step');
47
+ }
48
+ catch (e) {
49
+ setNote(e instanceof Error ? e.message : 'Could not reach this run');
50
+ }
51
+ finally {
52
+ setBusy(false);
53
+ }
54
+ };
55
+ const halt = async () => {
56
+ setNote('');
57
+ try {
58
+ await stop(t, id);
59
+ setNote('Stop requested — the run keeps its work on its branch');
60
+ }
61
+ catch (e) {
62
+ setNote(e instanceof Error ? e.message : 'Could not stop this run');
63
+ }
64
+ };
65
+ const title = detail.value?.title || (detail.loading ? '' : 'Untitled run');
66
+ return (_jsx(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", items: "center", children: _jsxs(YStack, { flex: 1, minH: 0, width: "100%", maxW: 816, 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(' · ') })] }), 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…' : '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, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), onStop: () => void halt(), busy: running && !draft.trim(), disabled: !running || busy, placeholder: running ? 'Steer this run' : 'This run has finished', label: "Steer this run" })] })] }) }));
67
+ }
package/lib/shelf.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function Artifacts(): import("react").JSX.Element;
2
+ export declare function Templates(): import("react").JSX.Element;
package/lib/shelf.js ADDED
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The two shelves the rail opens: what this org has built (Artifacts), and the
4
+ * public starters (Templates).
5
+ *
6
+ * A project is a real row — `/v1/projects`, the same one a deployed site is
7
+ * served from — so opening one opens its workspace at its own address. A
8
+ * starter is taken as a COPY of its own repository (`POST /v1/projects/fork`),
9
+ * so what opens is that template, not a model's imitation of it.
10
+ */
11
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
12
+ import { Boxes, ExternalLink, Globe } from '@hanzogui/lucide-icons-2';
13
+ import { useState } from 'react';
14
+ import { fork, templates } from './api/projects.js';
15
+ import { useProjects, useRead } from './data.js';
16
+ import { useHost, useTarget } from './host.js';
17
+ import { Out } from './out.js';
18
+ const SHOTS = 'https://hanzo.ai/templates';
19
+ function Head({ title, says }) {
20
+ return (_jsxs(YStack, { gap: "$1", pb: "$4", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }));
21
+ }
22
+ function Card({ onPress, label, children }) {
23
+ return (_jsx(YStack, { render: "button", onPress: onPress, "aria-label": label, flexBasis: 260, grow: 1, maxW: 360, minW: 0, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", overflow: "hidden", cursor: "pointer", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: children }));
24
+ }
25
+ export function Artifacts() {
26
+ const t = useTarget();
27
+ const host = useHost();
28
+ const list = useProjects(t, Boolean(host.person));
29
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((p) => (_jsx(Card, { label: `Open ${p.name}`, onPress: () => host.go(p.slug), children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Globe, { size: 14, opacity: 0.7 }), _jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.status || 'draft' })] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: p.live ? new URL(p.live).host : 'Not deployed yet' })] }) }, p.slug))) }))] }) }));
30
+ }
31
+ function Shot({ slug }) {
32
+ const [gone, setGone] = useState(false);
33
+ return (_jsxs(YStack, { height: 140, bg: "$hover", items: "center", justify: "center", position: "relative", overflow: "hidden", children: [_jsxs(YStack, { items: "center", gap: "$2", opacity: 0.6, children: [_jsx(Boxes, { size: 24 }), _jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: slug })] }), gone ? null : (_jsx("img", { src: `${SHOTS}/${encodeURIComponent(slug)}.webp`, alt: "", loading: "lazy", onError: () => setGone(true), style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' } }))] }));
34
+ }
35
+ export function Templates() {
36
+ const t = useTarget();
37
+ const host = useHost();
38
+ const list = useRead(() => templates(t), [], [t.api]);
39
+ const [busy, setBusy] = useState('');
40
+ const [failed, setFailed] = useState('');
41
+ const take = async (slug) => {
42
+ if (busy)
43
+ return;
44
+ if (!host.person)
45
+ return host.signIn?.();
46
+ setBusy(slug);
47
+ setFailed('');
48
+ try {
49
+ const made = await fork(t, slug);
50
+ host.go(made.slug || slug);
51
+ }
52
+ catch (e) {
53
+ setFailed(e instanceof Error ? e.message : 'Could not copy this template');
54
+ }
55
+ finally {
56
+ setBusy('');
57
+ }
58
+ };
59
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Templates", says: "Start from a working app. Picking one copies its repository into a project of yours." }), failed ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: failed })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((x) => (_jsxs(Card, { label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsxs(YStack, { p: "$3", gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: busy === x.slug ? 'Copying…' : x.title }), x.framework ? (_jsx(SizableText, { size: "$1", color: "$soft", children: x.framework })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })] })] }, x.slug))) })), _jsx(YStack, { pt: "$5", children: _jsxs(Out, { href: "https://gallery.hanzo.ai", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "The whole catalog" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })] }) }));
60
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ export interface Attached {
2
+ name: string;
3
+ text: string;
4
+ }
5
+ /** The ask with its attachments appended as fenced text. */
6
+ export declare function compose(prompt: string, files: Attached[]): string;
7
+ /** Read `list` into `have`, within the caps; answers the new list and what was refused. */
8
+ export declare function read(have: Attached[], list: FileList): Promise<{
9
+ files: Attached[];
10
+ note: string;
11
+ }>;
12
+ export declare function Attach({ files, onFiles, onNote }: {
13
+ files: Attached[];
14
+ onFiles: (f: Attached[]) => void;
15
+ onNote: (n: string) => void;
16
+ }): import("react").JSX.Element;
17
+ /** The attached files as small removable marks. */
18
+ export declare function Files({ files, onFiles }: {
19
+ files: Attached[];
20
+ onFiles: (f: Attached[]) => void;
21
+ }): import("react").JSX.Element;
22
+ export declare function Dictate({ onText, onNote }: {
23
+ onText: (text: string) => void;
24
+ onNote: (note: string) => void;
25
+ }): import("react").JSX.Element;