@hanzo/build 0.2.35 → 0.2.36

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 (2) hide show
  1. package/lib/project.js +60 -12
  2. package/package.json +1 -1
package/lib/project.js CHANGED
@@ -26,7 +26,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
26
26
  * template taken there reads `building` until its copy is `live` or failed, and
27
27
  * is read again until then; a run that publishes what it pushed says so on its
28
28
  * final status, which reads the project again and loads the preview again. Its
29
- * pull request is merged from beside its link.
29
+ * pull request is merged from beside its link. A build that failed, or that has
30
+ * run past STALL, says so in the preview with the platform's reason and offers
31
+ * Publish again, which builds the project's repository at its branch.
30
32
  *
31
33
  * Derived from the Hanzo App v2 editor (github.com/hanzoai/build-v2), itself
32
34
  * derived from OSW Studio and DeepSite (MIT). See NOTICE.
@@ -41,6 +43,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
41
43
  import { read as pushed } from './api/changes.js';
42
44
  import { start } from './api/coding.js';
43
45
  import { blob, tree } from './api/git.js';
46
+ import { declare } from './api/platform.js';
44
47
  import { deployments, name as repoName, ours, templates } from './api/projects.js';
45
48
  import { list, message, stop } from './api/sessions.js';
46
49
  import { decode, outcome, pull, said, who } from './api/turn.js';
@@ -57,6 +60,8 @@ import { Attach, compose, Dictate, Files } from './tools.js';
57
60
  const LIVE = new Set(['running', 'paused']);
58
61
  /** How often a project being published is read again, ms. */
59
62
  const AGAIN = 5000;
63
+ /** How long a build may run before the builder offers to publish again, s. A template's build takes a few minutes. */
64
+ const STALL = 10 * 60;
60
65
  /** The chat's width beside the work, px: its default, and the two it is kept between. */
61
66
  const CHAT_WIDTH = 360;
62
67
  const CHAT_FLOOR = 300;
@@ -89,14 +94,39 @@ export function Project({ slug }) {
89
94
  const building = project?.status === 'building';
90
95
  const failed = project?.status === 'error';
91
96
  const { reload: again } = all;
97
+ // The clock a build is measured against, moved each time the project is read again.
98
+ const [now, setNow] = useState(() => Date.now());
92
99
  useEffect(() => {
93
100
  if (!building)
94
101
  return;
95
- const every = setInterval(again, AGAIN);
102
+ const every = setInterval(() => {
103
+ again();
104
+ setNow(Date.now());
105
+ }, AGAIN);
96
106
  return () => clearInterval(every);
97
107
  }, [building, again]);
98
- // Why the build failed is its latest deployment's to say.
99
- const failure = useRead(failed ? () => deployments(t, slug) : null, [], [t, slug, failed, project?.updated]);
108
+ // When the build began, and why it failed, are its latest deployment's to say.
109
+ const failure = useRead(building || failed ? () => deployments(t, slug) : null, [], [t, slug, building, failed, project?.updated]);
110
+ const latest = failure.value[0];
111
+ const began = latest?.status === 'building' ? latest.created : 0;
112
+ const ran = began ? Math.max(0, Math.floor(now / 1000) - began) : 0;
113
+ const stalled = building && ran > STALL;
114
+ // Publish again: the project's repository, built at its branch, the way Publish builds it.
115
+ const [redo, setRedo] = useState({ busy: false, error: '' });
116
+ const retry = async () => {
117
+ if (!project?.repo || redo.busy)
118
+ return;
119
+ setRedo({ busy: true, error: '' });
120
+ try {
121
+ await declare(t, { repo: project.repo, ref: project.branch || 'main', name: slug, project: slug, mode: 'branch' });
122
+ setNow(Date.now());
123
+ again();
124
+ setRedo({ busy: false, error: '' });
125
+ }
126
+ catch (e) {
127
+ setRedo({ busy: false, error: e.message });
128
+ }
129
+ };
100
130
  const listed = useRead(signed ? () => list(t, { kind: 'coding', project: slug, limit: 50 }) : null, [], [t, signed, slug]);
101
131
  // Only the runs on this project's own repository: a record can be moved into
102
132
  // any project, and a run's branch is what Files, Code and Publish read.
@@ -314,18 +344,36 @@ export function Project({ slug }) {
314
344
  ? 'Reading this project’s runs…'
315
345
  : project?.live
316
346
  ? `${project.name} 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.`
317
- : building
318
- ? `${project?.name} is being published — it appears in the preview when its build finishes. Say what to change and it gets built.`
319
- : starter
320
- ? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
321
- : project
322
- ? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
323
- : `${slug} is loaded. 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(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_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 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : 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 ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : 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, { ...cut, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
347
+ : stalled
348
+ ? `${project?.name} has been publishing for ${Math.floor(ran / 60)} minutes without finishing. Publish it again from the preview, or say what to change and it gets built.`
349
+ : failed
350
+ ? `${project?.name} could not be published. Publish it again from the preview, or say what to change and it gets built.`
351
+ : building
352
+ ? `${project?.name} is being published — it appears in the preview when its build finishes. Say what to change and it gets built.`
353
+ : starter
354
+ ? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
355
+ : project
356
+ ? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
357
+ : `${slug} is loaded. 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(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_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 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : 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 ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : 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, { ...cut, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
324
358
  ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
325
359
  : 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 })] })] }), wide && !collapsed ? (_jsx(Grip, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", r: -GUTTER, width: GUTTER })) : null] }));
326
360
  // The preview stays loaded while another view is open, so coming back to it
327
361
  // is the page as it was left.
328
- const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_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, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), 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] }), _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: building ? (project?.forked ? 'Publishing your copy…' : 'Publishing…') : failed ? 'Publishing failed' : 'Nothing deployed yet' }), _jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: building ? 'Its page appears here when the build finishes.' : failed ? why : '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: () => {
362
+ const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_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, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), 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] }), _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: stalled
363
+ ? 'Publishing is taking too long'
364
+ : building
365
+ ? project?.forked
366
+ ? 'Publishing your copy…'
367
+ : 'Publishing…'
368
+ : failed
369
+ ? 'Publishing failed'
370
+ : 'Nothing deployed yet' }), _jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: stalled
371
+ ? `Its build started ${Math.floor(ran / 60)} minutes ago and has not finished.`
372
+ : building
373
+ ? 'Its page appears here when the build finishes.'
374
+ : failed
375
+ ? why
376
+ : 'Publish this project and its page appears here.' }), (stalled || failed) && project?.repo ? (_jsx(Button, { variant: "outline", disabled: redo.busy, onPress: () => void retry(), children: "Publish again" })) : null, redo.error ? (_jsx(SizableText, { size: "$2", color: "$soft", text: "center", role: "alert", children: redo.error })) : null] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
329
377
  const next = !editing;
330
378
  setEditing(next);
331
379
  frame.current?.post({ type: 'preview:editable', active: next });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.35",
3
+ "version": "0.2.36",
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 component, mounted by the Hanzo app at hanzo.ai.",