@hanzo/build 0.2.17 → 0.2.18

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/lib/ask.js CHANGED
@@ -20,7 +20,7 @@ function useAct(onDone) {
20
20
  onDone();
21
21
  }
22
22
  catch (e) {
23
- setNote(e instanceof Error ? e.message : 'That did not work');
23
+ setNote(e.message);
24
24
  setWorking(false);
25
25
  }
26
26
  };
@@ -63,7 +63,7 @@ export function Rename({ open, onOpenChange, title, name, run, }) {
63
63
  void go(() => run(next));
64
64
  };
65
65
  return (_jsx(Dialog, { open: open, onOpenChange: close, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: title }), _jsx(Input, { autoFocus: true, value: value, onChangeText: setValue, "aria-label": "Name", onKeyDown: (e) => {
66
- if ((e.key ?? e.nativeEvent?.key) === 'Enter')
66
+ if (e.key === 'Enter')
67
67
  save();
68
68
  } }), _jsx(Said, { note: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => close(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: save, children: "Save" })] })] }) }));
69
69
  }
package/lib/builder.js CHANGED
@@ -32,7 +32,7 @@ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, M
32
32
  import { Button } from '@hanzo/ui';
33
33
  import { SessionRail } from '@hanzo/ui/chat';
34
34
  import { HanzoMark } from '@hanzo/ui/product';
35
- import { useMemo, useState } from 'react';
35
+ import { useEffect, useMemo, useState } from 'react';
36
36
  import { pinBoard } from './choice.js';
37
37
  import { Customize } from './customize/index.js';
38
38
  import { useKept, useRecents } from './data.js';
@@ -49,6 +49,8 @@ import { Artifacts, Templates } from './shelf.js';
49
49
  import { Find } from './find.js';
50
50
  import { DOCS, Slack, Who } from './foot.js';
51
51
  import { Settings } from './settings/index.js';
52
+ import { Pitch } from './pitch.js';
53
+ import { Bar } from './visit.js';
52
54
  function Shell() {
53
55
  const host = useHost();
54
56
  const t = useTarget();
@@ -75,7 +77,7 @@ function Shell() {
75
77
  // it the search that finds a run. The organization is the account's, so it is
76
78
  // switched from the account menu at the foot. Beside the rail from md up, beside
77
79
  // the drawer's button below it — one place at a time.
78
- const name = (_jsx(XStack, { render: "button", "aria-label": "Hanzo Build", items: "center", onPress: () => go(''), children: _jsx(SizableText, { size: "$5", fontWeight: "600", color: "$ink", numberOfLines: 1, children: "Hanzo Build" }) }));
80
+ const name = _jsx(Name, { onPress: () => go('') });
79
81
  const links = [
80
82
  { id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
81
83
  { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
@@ -115,6 +117,31 @@ function Shell() {
115
117
  }
116
118
  : { name: 'Sign in', onPress: () => host.signIn?.() }, collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 92, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(YStack, { position: "absolute", t: 10, l: 8, r: 8, z: 2, gap: "$2.5", "$max-md": { display: 'none' }, children: [_jsx(XStack, { px: "$2.5", children: name }), _jsxs(XStack, { render: "button", "aria-label": "Search runs", onPress: () => setFinding(true), items: "center", gap: "$2", px: "$2.5", height: 34, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 15, color: "$soft" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Search" })] })] })), host.person ? _jsx(Who, { open: menu, onOpenChange: setMenu, narrow: collapsed }) : null] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 44, px: "$2", gap: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }), name, _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setFinding(true), "aria-label": "Search runs", children: _jsx(Search, { size: 18 }) })] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
117
119
  }
120
+ /** The wordmark: the product's name, as its host calls it, which leads to New. */
121
+ function Name({ onPress }) {
122
+ const { name } = useHost();
123
+ return (_jsx(XStack, { render: "button", "aria-label": name, items: "center", onPress: onPress, children: _jsx(SizableText, { size: "$5", fontWeight: "600", color: "$ink", numberOfLines: 1, children: name }) }));
124
+ }
125
+ /**
126
+ * A visitor: no runs to list, so the product's header stands where the rail
127
+ * would. On New the composer fills the first screen and the pitch follows it.
128
+ */
129
+ function Visit() {
130
+ const host = useHost();
131
+ const r = route(host.path);
132
+ // An address that names a part of the pitch (/#features) opens scrolled to it.
133
+ useEffect(() => {
134
+ const id = window.location.hash.slice(1);
135
+ if (id)
136
+ document.getElementById(id)?.scrollIntoView();
137
+ }, []);
138
+ const start = () => {
139
+ const page = document.getElementById('visit');
140
+ page?.scrollTo({ top: 0, behavior: 'smooth' });
141
+ page?.querySelector('textarea, [contenteditable="true"], input[type="text"]')?.focus();
142
+ };
143
+ return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsx(Bar, { name: _jsx(Name, { onPress: () => host.go('') }) }), r.kind === 'new' ? (_jsxs(YStack, { id: "visit", flex: 1, minH: 0, overflow: "scroll", children: [_jsx(YStack, { shrink: 0, style: { height: 'calc(100dvh - 60px)' }, children: _jsx(Pane, {}) }), _jsx(Pitch, { onStart: start })] })) : (_jsx(Pane, {}))] }));
144
+ }
118
145
  /** Whichever pane the address names, beside a rail — the builder's or the host's. */
119
146
  function Pane({ onStarted }) {
120
147
  const host = useHost();
@@ -143,7 +170,7 @@ function Pane({ onStarted }) {
143
170
  function Brand({ org }) {
144
171
  if (!org || org === 'hanzo')
145
172
  return _jsx(HanzoMark, { size: 18 });
146
- const letter = org.trim().charAt(0).toUpperCase() || 'H';
173
+ const letter = org.charAt(0).toUpperCase();
147
174
  return (_jsx(XStack, { width: 18, height: 18, rounded: "$1", items: "center", justify: "center", bg: "$raised", "aria-hidden": true, children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: letter }) }));
148
175
  }
149
176
  function Screens({ rail }) {
@@ -151,7 +178,12 @@ function Screens({ rail }) {
151
178
  const r = route(host.path);
152
179
  if (r.kind === 'project')
153
180
  return _jsx(Project, { slug: r.slug }, r.slug);
154
- return rail ? _jsx(Shell, {}) : _jsx(Pane, {});
181
+ if (!rail)
182
+ return _jsx(Pane, {});
183
+ // Signed out with no token to wait on: a token still resolving is a person, not a visitor.
184
+ if (!host.person && !host.token() && host.signIn)
185
+ return _jsx(Visit, {});
186
+ return _jsx(Shell, {});
155
187
  }
156
188
  /**
157
189
  * The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
package/lib/choice.js CHANGED
@@ -29,7 +29,7 @@ function write(org, value) {
29
29
  /** Point New at a forge codebase, and optionally a first draft. */
30
30
  export function pinCodebase(org, c, ask = '') {
31
31
  const repo = asRepo(c);
32
- write(org, { ...read(org), repo, branch: repo.default_branch || 'main', ask });
32
+ write(org, { ...read(org), repo, branch: repo.default_branch, ask });
33
33
  }
34
34
  /** Which board Issues is showing. '' is every board. */
35
35
  export function pinBoard(org, key) {
@@ -39,7 +39,7 @@ export function pinBoard(org, key) {
39
39
  catch {
40
40
  /* Issues then shows every board */
41
41
  }
42
- window.dispatchEvent(new Event('hanzo-board'));
42
+ window.dispatchEvent(new CustomEvent('hanzo-board', { detail: key }));
43
43
  }
44
44
  export const isForge = (repo) => Boolean(repo && repo.forge === true && repo.name);
45
45
  const syncingKey = (org) => `hanzo.build.sync.${org ?? 'none'}`;
package/lib/data.js CHANGED
@@ -32,7 +32,8 @@ export function useRead(load, initial, key) {
32
32
  setValue(v);
33
33
  setError(null);
34
34
  })
35
- .catch((e) => live && setError(e instanceof Error ? e : new Error(String(e))))
35
+ // Every read goes through the platform's client, which refuses with an Error.
36
+ .catch((e) => live && setError(e))
36
37
  .finally(() => live && setLoading(false));
37
38
  return () => {
38
39
  live = false;
@@ -50,27 +51,28 @@ export function useRecents(t, signed) {
50
51
  const read = useRead(signed ? () => list(t, { kind: 'coding', limit: 50 }) : null, [], [t, signed]);
51
52
  const [rows, setRows] = useState([]);
52
53
  useEffect(() => setRows(read.value), [read.value]);
53
- // A feed of the whole org carries every agent's heartbeat, so an unseen id
54
- // re-reads the list at most every five seconds.
55
54
  const last = useRef(0);
56
- const reload = useRef(() => { });
57
- reload.current = () => {
58
- const now = Date.now();
59
- if (now - last.current < 5000)
60
- return;
61
- last.current = now;
62
- read.reload();
63
- };
55
+ // useRead's reload is the same function every render, so it never reopens the feed.
56
+ const again = read.reload;
64
57
  useEffect(() => {
65
58
  if (!signed)
66
59
  return;
60
+ // A feed of the whole org carries every agent's heartbeat, so an unseen id
61
+ // re-reads the list at most every five seconds.
62
+ const reload = () => {
63
+ const now = Date.now();
64
+ if (now - last.current < 5000)
65
+ return;
66
+ last.current = now;
67
+ again();
68
+ };
67
69
  const ctl = new AbortController();
68
70
  void watch(t, '', {
69
71
  session: (s) => setRows((prev) => {
70
72
  const i = prev.findIndex((r) => r.id === s.id);
71
73
  if (i === -1) {
72
74
  // A run this list has not seen: re-read rather than guess its kind.
73
- reload.current();
75
+ reload();
74
76
  return prev;
75
77
  }
76
78
  const next = prev.slice();
@@ -79,13 +81,13 @@ export function useRecents(t, signed) {
79
81
  }),
80
82
  open: (n) => {
81
83
  if (n > 0)
82
- reload.current();
84
+ reload();
83
85
  },
84
86
  }, ctl.signal).catch(() => {
85
87
  /* a refused feed leaves the list as read */
86
88
  });
87
89
  return () => ctl.abort();
88
- }, [t, signed]);
90
+ }, [t, signed, again]);
89
91
  return { ...read, value: rows };
90
92
  }
91
93
  export function usePlaces(t, signed) {
@@ -149,7 +151,7 @@ export function useRun(t, id) {
149
151
  if (n > 0)
150
152
  reload.current();
151
153
  },
152
- }, ctl.signal).catch((e) => setRefused(e instanceof Error ? e.message : 'The feed was refused'));
154
+ }, ctl.signal).catch((e) => setRefused(e.message));
153
155
  return () => ctl.abort();
154
156
  }, [t, id]);
155
157
  // The read and the frames describe THIS run or nothing: a read kept from the
package/lib/find.js CHANGED
@@ -44,7 +44,7 @@ export function Find({ open, onOpenChange, recents, onOpen, }) {
44
44
  })
45
45
  .catch((e) => {
46
46
  if (mine === asked.current)
47
- setError(e instanceof Error ? e.message : 'The runs could not be read');
47
+ setError(e.message);
48
48
  })
49
49
  .finally(() => {
50
50
  if (mine === asked.current)
@@ -71,7 +71,7 @@ export function Find({ open, onOpenChange, recents, onOpen, }) {
71
71
  return n ? all.filter((h) => `${h.title} ${h.line}`.toLowerCase().includes(n)) : all;
72
72
  }, [q, rows, recents, status]);
73
73
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, children: [_jsx(DialogTitle, { children: "Find a run" }), _jsx(Input, { autoFocus: true, value: q, onChangeText: setQ, placeholder: "Search runs\u2026", "aria-label": "Search runs", onKeyDown: (e) => {
74
- if ((e.key ?? e.nativeEvent?.key) === 'Enter' && hits[0])
74
+ if (e.key === 'Enter' && hits[0])
75
75
  onOpen(hits[0].id);
76
76
  } }), _jsx(XStack, { gap: "$1", flexWrap: "wrap", role: "group", "aria-label": "Status", children: FILTERS.map((f) => (_jsx(XStack, { render: "button", "aria-label": f.label, "aria-pressed": status === f.id, onPress: () => setStatus(f.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: status === f.id ? '$ink' : '$borderColor', bg: status === f.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$1", color: status === f.id ? '$ink' : '$soft', children: f.label }) }, f.id || 'all'))) }), _jsx(ScrollView, { maxH: 360, children: _jsxs(YStack, { gap: "$1", role: "list", children: [hits.map((h) => (_jsxs(XStack, { role: "listitem", render: "button", "aria-label": h.title, onPress: () => onOpen(h.id), px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, items: "center", gap: "$3", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: h.title }), h.line ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, shrink: 0, maxW: "45%", children: h.line })) : null] }, h.id))), !loading && !error && hits.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", children: rows && next ? 'No run read so far matches.' : 'No run matches.' })) : null] }) }), error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: error })) : null, loading ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: "Reading runs\u2026" })) : next ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => void read(next), children: "Older runs" }) })) : null] }) }));
77
77
  }
package/lib/forge.js CHANGED
@@ -69,7 +69,7 @@ function Automations() {
69
69
  setTick((n) => n + 1);
70
70
  }
71
71
  catch (e) {
72
- setProblem(e instanceof Error ? e.message : 'Could not change that automation');
72
+ setProblem(e.message);
73
73
  }
74
74
  };
75
75
  return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: "Automations" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Repeating work for this organization. A new one starts off, so you can name it before it runs." })] }), _jsxs(Button, { size: "sm", shrink: 0, disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New automation"] })] }), _jsx(XStack, { justify: "flex-end", children: _jsx(YStack, { width: 240, maxW: "100%", children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search\u2026", "aria-label": "Search automations", disabled: !signed }) }) }), problem ? _jsx(Note, { children: problem }) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Empty, { says: "Sign in to see this organization's automations." })) : list.loading && list.value.length === 0 ? (_jsx(Empty, { says: "Reading automations\u2026" })) : shown.length === 0 ? (_jsx(Empty, { says: list.value.length === 0 ? 'No automations yet.' : 'Nothing matches.', children: list.value.length === 0 ? (_jsx(Button, { size: "sm", onPress: () => setMaking(true), children: "New automation" })) : null })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Name" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 80, style: { textAlign: 'right' }, children: "Status" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: "Updated" })] }), shown.map((a) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: a.name }), _jsx(XStack, { render: "button", "aria-label": a.status === 'ENABLED' ? `Turn off ${a.name}` : `Turn on ${a.name}`, onPress: () => void flip(a), width: 80, justify: "flex-end", children: _jsx(SizableText, { size: "$1", color: "$ink", children: a.status === 'ENABLED' ? 'On' : 'Off' }) }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: a.updated ? when(new Date(a.updated).toISOString()) : '—' })] }, a.id)))] }))] }), _jsx(NewAutomation, { open: making, onOpenChange: setMaking, onCreate: async (name) => {
@@ -93,7 +93,7 @@ function NewAutomation({ open, onOpenChange, onCreate, }) {
93
93
  setName('');
94
94
  }
95
95
  catch (e) {
96
- setError(e instanceof Error ? e.message : 'Could not create that automation');
96
+ setError(e.message);
97
97
  }
98
98
  finally {
99
99
  setBusy(false);
@@ -198,7 +198,7 @@ function NewRepo({ open, onOpenChange, onCreate, }) {
198
198
  setDescription('');
199
199
  }
200
200
  catch (e) {
201
- setError(e instanceof Error ? e.message : 'Could not create that repository');
201
+ setError(e.message);
202
202
  }
203
203
  finally {
204
204
  setBusy(false);
@@ -240,20 +240,12 @@ function Issues() {
240
240
  const signed = Boolean(host.person);
241
241
  const [board, setBoard] = useKept(boardKey(host.org), '');
242
242
  const list = useRead(signed ? () => issues(t, board) : null, [], [t, signed, board]);
243
+ // Another screen chose the board (the rail's Issues chooses every board) while this one is open.
243
244
  useEffect(() => {
244
- const on = () => {
245
- try {
246
- const raw = window.localStorage.getItem(boardKey(host.org));
247
- const next = raw === null ? '' : JSON.parse(raw);
248
- setBoard(typeof next === 'string' ? next : '');
249
- }
250
- catch {
251
- setBoard('');
252
- }
253
- };
245
+ const on = (e) => setBoard(e.detail);
254
246
  window.addEventListener('hanzo-board', on);
255
247
  return () => window.removeEventListener('hanzo-board', on);
256
- }, [host.org, setBoard]);
248
+ }, [setBoard]);
257
249
  const open = (w) => {
258
250
  const name = w.repo || w.project;
259
251
  if (name) {
package/lib/host.d.ts CHANGED
@@ -17,6 +17,8 @@ export interface Person {
17
17
  avatar: string;
18
18
  }
19
19
  export interface Host {
20
+ /** The product's name, drawn at the top left: `Hanzo Build` on hanzo.build, `Hanzo` on hanzo.ai. */
21
+ name: string;
20
22
  /** The platform origin, no trailing slash. */
21
23
  api: string;
22
24
  /** The signed-in person's bearer, or null. Read at call time, so a refresh is picked up. */
package/lib/landing.js CHANGED
@@ -41,6 +41,13 @@ export const EFFORTS = [
41
41
  ];
42
42
  const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium', ask: '' };
43
43
  const COLUMN = 768;
44
+ /**
45
+ * What was typed here and not sent, for as long as this document lives. Signing
46
+ * in swaps a visitor's page for the rail, which draws New again in another
47
+ * place; the popup keeps the document so the sentence can come along, and this
48
+ * is where it waits.
49
+ */
50
+ let unsent = { draft: '', files: [] };
44
51
  export function Landing({ onStarted }) {
45
52
  const host = useHost();
46
53
  const t = useTarget();
@@ -50,8 +57,11 @@ export function Landing({ onStarted }) {
50
57
  // Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
51
58
  const kept = stored ?? { ...FIRST, ...prefs.code };
52
59
  const set = (patch) => keep({ ...kept, ...patch });
53
- const [draft, setDraft] = useState(kept.ask || '');
54
- const [files, setFiles] = useState([]);
60
+ const [draft, setDraft] = useState(kept.ask || unsent.draft);
61
+ const [files, setFiles] = useState(unsent.files);
62
+ useEffect(() => {
63
+ unsent = { draft, files };
64
+ }, [draft, files]);
55
65
  const [busy, setBusy] = useState(false);
56
66
  const [note, setNote] = useState('');
57
67
  const [adding, setAdding] = useState(null);
@@ -81,18 +91,16 @@ export function Landing({ onStarted }) {
81
91
  known.current.set(r.name, r);
82
92
  return { repos, next: null };
83
93
  }, [t]);
94
+ // Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
95
+ const home = kept.repo?.default_branch || 'main';
84
96
  const loadBranches = useCallback(async (q) => {
85
- if (!kept.repo)
86
- return { branches: [], next: null };
87
- const found = await one(t, kept.repo.name);
97
+ const found = await one(t, codebase);
88
98
  const needle = q.trim().toLowerCase();
89
- const names = (found?.branches ?? [kept.repo.default_branch || 'main']).filter((b) => !needle || b.toLowerCase().includes(needle));
99
+ const names = (found?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
90
100
  return { branches: names.map((name) => ({ name })), next: null };
91
- }, [t, kept.repo]);
101
+ }, [t, codebase, home]);
102
+ // The composer sends only a draft with words in it, and not while a send is out.
92
103
  const send = async () => {
93
- const prompt = draft.trim();
94
- if (!prompt || busy)
95
- return;
96
104
  if (!signed) {
97
105
  host.signIn?.();
98
106
  return;
@@ -105,11 +113,12 @@ export function Landing({ onStarted }) {
105
113
  setNote('');
106
114
  try {
107
115
  const run = await start(t, {
108
- prompt: compose(prompt, files),
116
+ prompt: compose(draft.trim(), files),
109
117
  // The name alone. The org rides the request, and a slash is not a repo name.
110
- repo: isForge(kept.repo) ? kept.repo.name : undefined,
111
- base: isForge(kept.repo) ? kept.branch || kept.repo.default_branch || undefined : undefined,
112
- targetId: place.id || undefined,
118
+ // An empty base or place is not sent: the default branch, and the sandbox.
119
+ repo: kept.repo.name,
120
+ base: kept.branch,
121
+ targetId: place.id,
113
122
  mode: kept.mode,
114
123
  model: kept.model === ENSO ? undefined : kept.model,
115
124
  effort: kept.effort,
@@ -119,7 +128,7 @@ export function Landing({ onStarted }) {
119
128
  onStarted(run.session);
120
129
  }
121
130
  catch (e) {
122
- setNote(e instanceof Error ? e.message : 'The run could not start');
131
+ setNote(e.message);
123
132
  }
124
133
  finally {
125
134
  setBusy(false);
@@ -146,7 +155,7 @@ export function Landing({ onStarted }) {
146
155
  place: p,
147
156
  })), [places.value, setUp]);
148
157
  const branch = kept.branch || kept.repo?.default_branch || 'main';
149
- 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 : 'Cloud', 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: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
158
+ 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 : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
150
159
  const row = known.current.get(r.name) ?? chosen(r);
151
160
  set({ repo: row, branch: row.default_branch });
152
161
  }, load: loadRepos, troubleshoot: {
package/lib/markdown.js CHANGED
@@ -5,7 +5,8 @@ const RULE = /^\s{0,3}([-*_])(\s*\1){2,}\s*$/;
5
5
  const QUOTE = /^\s{0,3}>\s?(.*)$/;
6
6
  const ROW = /^\s*\|(.*)\|\s*$/;
7
7
  const DIVIDER = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
8
- const cells = (line) => (ROW.exec(line)?.[1] ?? line).split('|').map((c) => c.trim());
8
+ /** A table row's cells. Only a line ROW matched is one. */
9
+ const cells = (line) => ROW.exec(line)[1].split('|').map((c) => c.trim());
9
10
  /** The blocks of a markdown text, in order. */
10
11
  export function blocks(md) {
11
12
  const lines = md.replace(/\r\n?/g, '\n').split('\n');
@@ -25,7 +26,7 @@ export function blocks(md) {
25
26
  i++;
26
27
  while (i < lines.length && !lines[i].trim().startsWith(fence[1]))
27
28
  body.push(lines[i++]);
28
- out.push({ kind: 'code', lang: fence[2] ?? '', text: body.join('\n') });
29
+ out.push({ kind: 'code', lang: fence[2], text: body.join('\n') });
29
30
  continue;
30
31
  }
31
32
  if (!line.trim()) {
@@ -78,7 +79,7 @@ export function blocks(md) {
78
79
  continue;
79
80
  }
80
81
  // A line indented under an item continues it.
81
- if (lines[j].trim() && /^\s{2,}/.test(lines[j]) && items.length) {
82
+ if (lines[j].trim() && /^\s{2,}/.test(lines[j])) {
82
83
  items[items.length - 1].text += ` ${lines[j].trim()}`;
83
84
  j++;
84
85
  continue;
@@ -113,7 +114,8 @@ export function spans(text) {
113
114
  out.push({ kind: 'strong', text: (m[5] ?? m[6]) });
114
115
  else if (m[7] !== undefined || m[8] !== undefined)
115
116
  out.push({ kind: 'em', text: (m[7] ?? m[8]) });
116
- else if (m[9] !== undefined)
117
+ // What is left is the bare address, the only other thing INLINE matches.
118
+ else
117
119
  out.push({ kind: 'link', text: m[9], href: m[9] });
118
120
  at = m.index + m[0].length;
119
121
  }
package/lib/pitch.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare const INSTALL = "curl -fsSL hanzo.sh | sh";
2
+ export declare function Pitch({ onStart }: {
3
+ onStart: () => void;
4
+ }): import("react").JSX.Element;
package/lib/pitch.js ADDED
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * What a visitor reads under New on hanzo.build: the same agent on their own
4
+ * computer, one line to install, and what it brings to a run. Each card names
5
+ * something this page or the CLI does today, and nothing it does not.
6
+ */
7
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
8
+ import { Check, Copy, GitPullRequest, LayoutTemplate, ListChecks, MonitorPlay, Plug, Server, Sparkles, Workflow } from '@hanzogui/lucide-icons-2';
9
+ import { Button } from '@hanzo/ui';
10
+ import { useState } from 'react';
11
+ import { DOCS } from './foot.js';
12
+ export const INSTALL = 'curl -fsSL hanzo.sh | sh';
13
+ const FEATURES = [
14
+ { icon: _jsx(Sparkles, { size: 18 }), title: 'Skills', body: 'Teach it a workflow once. A skill loads when a task matches, or when you call it by name.' },
15
+ { icon: _jsx(ListChecks, { size: 18 }), title: 'Plan first', body: 'Start a run in plan mode. Nothing is built until you approve the plan.' },
16
+ { icon: _jsx(Plug, { size: 18 }), title: 'Connectors and plugins', body: 'MCP servers, skills and agents behind one install. Your org’s credentials reach a run through a relay, never the sandbox.' },
17
+ { icon: _jsx(Server, { size: 18 }), title: 'Environments', body: 'Point it at a repository. An agent works out install and start, and every later run begins from that setup.' },
18
+ { icon: _jsx(MonitorPlay, { size: 18 }), title: 'Desktop and terminal', body: 'Watch each run’s browser and shell live, beside the conversation.' },
19
+ { icon: _jsx(GitPullRequest, { size: 18 }), title: 'Review', body: 'Every run ends as a diff on its own branch, with commits and a pull request to review.' },
20
+ { icon: _jsx(LayoutTemplate, { size: 18 }), title: 'Templates to live', body: 'Start from a working app and publish it to a live address.' },
21
+ { icon: _jsx(Workflow, { size: 18 }), title: 'Automations', body: 'Hand it repeating work, and it runs on your schedule.' },
22
+ ];
23
+ /** One line to copy, with a button that copies it. */
24
+ function Line({ text }) {
25
+ const [copied, setCopied] = useState(false);
26
+ const copy = () => {
27
+ void navigator.clipboard.writeText(text).then(() => {
28
+ setCopied(true);
29
+ setTimeout(() => setCopied(false), 1500);
30
+ });
31
+ };
32
+ return (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", height: 52, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", width: "100%", maxW: 560, children: [_jsx(SizableText, { size: "$3", color: "$soft", fontFamily: "$mono", "aria-hidden": true, children: "$" }), _jsx(SizableText, { flex: 1, size: "$3", color: "$ink", fontFamily: "$mono", numberOfLines: 1, children: text }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": copied ? 'Copied' : 'Copy the install command', onPress: copy, children: copied ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }) })] }));
33
+ }
34
+ function Card({ icon, title, body }) {
35
+ return (_jsxs(YStack, { gap: "$2", p: "$5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flex: 1, minW: 240, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [icon, _jsx(SizableText, { size: "$4", fontWeight: "600", color: "$ink", children: title })] }), _jsx(SizableText, { size: "$3", color: "$soft", children: body })] }));
36
+ }
37
+ export function Pitch({ onStart }) {
38
+ return (_jsxs(YStack, { items: "center", px: "$4", pb: "$12", gap: "$12", children: [_jsxs(YStack, { id: "features", width: "100%", maxW: 1080, items: "center", gap: "$5", pt: "$6", children: [_jsx(SizableText, { size: "$2", color: "$soft", fontFamily: "$mono", children: "Powered by Zen 6 \u00B7 routed by Enso" }), _jsx(SizableText, { role: "heading", "aria-level": 2, size: "$9", color: "$ink", style: { textAlign: 'center' }, children: "Hanzo Dev" }), _jsx(SizableText, { size: "$4", color: "$soft", maxW: 620, style: { textAlign: 'center' }, children: "The agent. It runs in your terminal, on any codebase, in any language. This page is that same agent in the browser." }), _jsx(Line, { text: INSTALL }), _jsx(SizableText, { size: "$2", color: "$soft", fontFamily: "$mono", children: "dev opens it. dev \"fix the failing test\" hands it a task." }), _jsx("a", { href: DOCS, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: "Read the docs" }) }) })] }), _jsxs(YStack, { width: "100%", maxW: 1080, gap: "$5", children: [_jsx(SizableText, { role: "heading", "aria-level": 2, size: "$7", color: "$ink", children: "Everything a run needs" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: FEATURES.map((f) => (_jsx(Card, { ...f }, f.title))) })] }), _jsxs(YStack, { width: "100%", maxW: 1080, items: "center", gap: "$4", py: "$8", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(SizableText, { role: "heading", "aria-level": 2, size: "$7", color: "$ink", style: { textAlign: 'center' }, children: "Say what you want built." }), _jsx(SizableText, { size: "$3", color: "$soft", style: { textAlign: 'center' }, px: "$4", children: "In the browser here, or in your terminal." }), _jsx(Button, { variant: "primary", size: "sm", onPress: onStart, children: "Start building" })] })] }));
39
+ }
package/lib/plans.js CHANGED
@@ -37,6 +37,7 @@ export function Plans() {
37
37
  const [working, setWorking] = useState(false);
38
38
  const [note, setNote] = useState('');
39
39
  const on = current(subs.value);
40
+ const who = host.org ?? 'This organization';
40
41
  const card = chosen(cards.value);
41
42
  const yearly = list.value.some((p) => p.yearly > 0);
42
43
  const free = (p) => p.monthly === 0 && !p.sales;
@@ -51,7 +52,7 @@ export function Plans() {
51
52
  subs.reload();
52
53
  }
53
54
  catch (e) {
54
- setNote(e instanceof Error ? e.message : 'That did not work');
55
+ setNote(e.message);
55
56
  }
56
57
  finally {
57
58
  setWorking(false);
@@ -73,10 +74,10 @@ export function Plans() {
73
74
  if (!settled)
74
75
  return null;
75
76
  if (free(p)) {
76
- if (!on)
77
- return null;
78
- if (on.ending)
79
- return _jsx(Line, { children: `You move to ${p.name} when ${on.name} ends on ${day(on.ends)}.` });
77
+ // A free plan that is not the org's means a paid one is.
78
+ const paid = on;
79
+ if (paid.ending)
80
+ return _jsx(Line, { children: `You move to ${p.name} when ${paid.name} ends on ${day(paid.ends)}.` });
80
81
  return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: `Switch to ${p.name}` }));
81
82
  }
82
83
  if (on && on.price > 0)
@@ -89,17 +90,17 @@ export function Plans() {
89
90
  return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 1100, mx: "auto", px: "$5", py: "$5", gap: "$5", children: [_jsxs(XStack, { render: "button", "aria-label": "Back to billing", items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Billing" })] }), _jsxs(XStack, { items: "flex-end", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 240, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", children: "Plans that grow with you" }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
90
91
  ? 'Sign in to see the plan you are on.'
91
92
  : on
92
- ? `${host.org ?? 'This organization'} is on ${on.name}.`
93
- : `${host.org ?? 'This organization'} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
93
+ ? `${who} is on ${on.name}.`
94
+ : `${who} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
94
95
  const cost = price(p, term);
95
96
  return (_jsxs(YStack, { "aria-label": `${p.name} plan`, flex: 1, minW: 240, maxW: 360, "$max-sm": { maxW: '100%' }, gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: mine(p) || p.popular ? '$ink' : '$borderColor', bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$5", color: "$ink", children: p.name }), mine(p) ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Current" })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Popular" })) : null] }), p.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: p.description })) : null, _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$7", color: "$ink", children: p.sales ? 'Custom' : cost.month === 0 ? 'Free' : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
96
97
  ? 'Talk to us'
97
98
  : cost.month === 0
98
99
  ? 'Forever'
99
100
  : `${p.perSeat ? 'per seat, ' : ''}a month${term === 'year' && p.yearly ? `, ${money(cost.now, p.currency)} billed yearly` : ''}` })] }), _jsx(XStack, { children: move(p) }), p.features.length ? (_jsx(YStack, { gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: p.features.map((f) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(YStack, { pt: 2, children: _jsx(Check, { size: 14 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$soft", children: f })] }, f))) })) : null] }, p.id));
100
- }) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => (o ? undefined : setBuying(null)), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
101
+ }) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => !o && setBuying(null), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
101
102
  await subscribe(t, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
102
- return `${host.org ?? 'This organization'} is on ${buying.name}`;
103
+ return `${who} is on ${buying.name}`;
103
104
  }), children: "Upgrade" })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Switch to the free plan?" }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${on.name} stays on until ${day(on.ends) || 'the end of this period'}, and nothing is charged after that.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: `Keep ${on.name}` }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
104
105
  await cancel(t, on.id);
105
106
  return `${on.name} ends on ${day(on.ends)}`;