@hanzo/build 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,12 @@ import { Builder, DevSection } from '@hanzo/build'
79
79
  <Builder host={host} rail={false} />
80
80
  ```
81
81
 
82
+ A run draws its own panes — the transcript, the side pane a gutter beside it
83
+ with the resize edge in that gutter, and each composer — from @hanzo/design's
84
+ pane tokens: `--pane-fill`, `--pane-round`, `--pane-edge`, `--pane-blur`,
85
+ `--pane-gap`, and `--shadow-sheet-2` for the drop. A host retunes the tokens;
86
+ it writes no rule against the builder's slots.
87
+
82
88
  `DevSection` is rows of `@hanzo/ui/chat`'s Sidebar; `useSessions(host)` is its
83
89
  data — the org's coding runs, kept live by the org's feed — for a host that
84
90
  draws the rows itself. `administers(token, org)` reads the org-admin bit off a
@@ -260,6 +266,7 @@ src/
260
266
  transcript.tsx a run's transcript, as cards
261
267
  prose.tsx markdown, drawn as text (markdown.ts reads it)
262
268
  desk.tsx the pane beside a run
269
+ pane.ts how a pane is cut, from the --pane-* tokens
263
270
  door.tsx a run's desktop or shell, framed
264
271
  git.tsx what a run pushed
265
272
  files.tsx a run's files and artifacts
package/lib/builder.d.ts CHANGED
@@ -13,7 +13,8 @@ export declare function nav(host: Host, go: (path: string) => void): {
13
13
  * The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
14
14
  *
15
15
  * `rail={false}` leaves the left column to the host, which lists the builder's
16
- * places and runs in its own rail with `DevSection`.
16
+ * places and runs in its own rail with `DevSection`, and the ground to it too:
17
+ * the builder is then a pane of the host's, and paints no background of its own.
17
18
  */
18
19
  export declare function Builder({ host, rail }: {
19
20
  host: Host;
package/lib/builder.js CHANGED
@@ -178,8 +178,9 @@ function Screens({ rail }) {
178
178
  * The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
179
179
  *
180
180
  * `rail={false}` leaves the left column to the host, which lists the builder's
181
- * places and runs in its own rail with `DevSection`.
181
+ * places and runs in its own rail with `DevSection`, and the ground to it too:
182
+ * the builder is then a pane of the host's, and paints no background of its own.
182
183
  */
183
184
  export function Builder({ host, rail = true }) {
184
- return (_jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: "$background", children: _jsx(Screens, { rail: rail }) }) }) }));
185
+ return (_jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: rail ? '$background' : undefined, children: _jsx(Screens, { rail: rail }) }) }) }));
185
186
  }
package/lib/desk.js CHANGED
@@ -39,7 +39,7 @@ export function Desk({ id, repo, branch, base, environment, mode, pr, title, pro
39
39
  const box = useBox(sandbox);
40
40
  // Nothing is said of the sandbox until it has been read.
41
41
  const held = box.value?.state ?? '';
42
- return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "browser", tab: tab, onPick: setTab, children: "Browser" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "artifacts", tab: tab, onPick: setTab, children: "Artifacts" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" })] }), _jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), sandbox ? _jsx(Held, { box: box, live: live }) : null, _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Sandbox", value: sandbox || '—' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { sandbox: sandbox, held: held, site: site, live: live }, published) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, held: held, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, held: held, lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { session: id, sandbox: sandbox, repo: repo, branch: branch, mode: mode, project: project, site: site, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'files' ? _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: held === 'running' }) : null] })] }));
42
+ return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, overflow: "hidden", style: { borderRadius: 'inherit' }, children: [_jsxs(XStack, { px: "$2", py: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "browser", tab: tab, onPick: setTab, children: "Browser" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "artifacts", tab: tab, onPick: setTab, children: "Artifacts" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" })] }), _jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), sandbox ? _jsx(Held, { box: box, live: live }) : null, _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Sandbox", value: sandbox || '—' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { sandbox: sandbox, held: held, site: site, live: live }, published) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, held: held, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, held: held, lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { session: id, sandbox: sandbox, repo: repo, branch: branch, mode: mode, project: project, site: site, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'files' ? _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: held === 'running' }) : null] })] }));
43
43
  }
44
44
  /**
45
45
  * The run's sandbox as it stands, and what moves it. It is read while the pane is
@@ -210,7 +210,7 @@ function Site({ site, live, held }) {
210
210
  }
211
211
  function TabButton({ id, tab, onPick, children }) {
212
212
  const on = tab === id;
213
- return (_jsx(XStack, { render: "button", "aria-label": children, onPress: () => onPick(id), px: "$2", py: "$2", borderBottomWidth: 2, borderColor: on ? '$ink' : 'transparent', children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', children: children }) }));
213
+ return (_jsx(XStack, { render: "button", "aria-label": children, "aria-pressed": on, onPress: () => onPick(id), px: "$3", py: "$1.5", rounded: 999, shrink: 0, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', children: children }) }));
214
214
  }
215
215
  function Fact({ label, value }) {
216
216
  return (_jsxs(XStack, { gap: "$3", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 96, children: label }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 2, children: value })] }));
package/lib/foot.js CHANGED
@@ -15,7 +15,7 @@ import { path } from './route.js';
15
15
  export const DOCS = 'https://docs.hanzo.ai/docs/dev';
16
16
  /** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
17
17
  export function Slack({ onOpen, onDismiss }) {
18
- return (_jsxs(XStack, { items: "center", gap: "$2.5", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(Mark, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: "Try Hanzo in Slack" }), _jsx(XStack, { render: "button", "aria-label": "Set up Hanzo in Slack", onPress: onOpen, cursor: "pointer", children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Set up" }) })] }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: onDismiss, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 14 }) })] }));
18
+ return (_jsxs(XStack, { items: "center", gap: "$2.5", px: "$3", py: "$2.5", rounded: "var(--radius-lg)", borderWidth: 1, borderColor: "$borderColor", bg: "var(--glass)", children: [_jsx(Mark, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: "Try Hanzo in Slack" }), _jsx(XStack, { render: "button", "aria-label": "Set up Hanzo in Slack", onPress: onOpen, cursor: "pointer", children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Set up" }) })] }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: onDismiss, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 14 }) })] }));
19
19
  }
20
20
  /**
21
21
  * The account menu's open state, for whatever row opens it.
package/lib/landing.js CHANGED
@@ -29,6 +29,7 @@ import { ready, SANDBOX } from './api/places.js';
29
29
  import { isForge } from './choice.js';
30
30
  import { useKept, usePlaces, useRead } from './data.js';
31
31
  import { useHost, useTarget } from './host.js';
32
+ import { pane } from './pane.js';
32
33
  import { usePrefs } from './prefs.js';
33
34
  import { Publish } from './publish.js';
34
35
  import { path } from './route.js';
@@ -211,7 +212,7 @@ export function Landing({ onStarted }) {
211
212
  const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
212
213
  return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", px: "$4", pb: "$10", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, gap: "$5", children: [_jsxs(XStack, { role: "heading", "aria-level": 1, justify: "center", items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(HanzoMark, { size: 26 }), _jsx(SizableText, { size: "$8", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' })] }), _jsxs(YStack, { gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
213
214
  ? `A proposed environment for ${codebase} is waiting for review.`
214
- : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
215
+ : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, ...pane, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
215
216
  setOffering(false);
216
217
  env.reload();
217
218
  } })) : null] }));
package/lib/pane.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * How a pane is cut: a card floating on the host's ground, read from
3
+ * @hanzo/design's `--pane-*` tokens. Each falls back to the design value the
4
+ * token names, so a host that sets none still draws a pane.
5
+ *
6
+ * pane fill, corner, edge, lens and drop, as gui props
7
+ * gap the gutter between two panes, and the width of the edge in it
8
+ */
9
+ export declare const pane: {
10
+ readonly bg: 'var(--pane-fill, var(--sheet-1))';
11
+ readonly rounded: 'var(--pane-round, var(--radius-xl))';
12
+ readonly borderWidth: 1;
13
+ readonly borderColor: 'var(--pane-edge, var(--white-08))';
14
+ readonly backdropFilter: 'var(--pane-blur, blur(20px) saturate(1.8))';
15
+ readonly boxShadow: 'var(--shadow-sheet-2)';
16
+ };
17
+ export declare const gap = "var(--pane-gap, var(--space-2))";
package/lib/pane.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * How a pane is cut: a card floating on the host's ground, read from
3
+ * @hanzo/design's `--pane-*` tokens. Each falls back to the design value the
4
+ * token names, so a host that sets none still draws a pane.
5
+ *
6
+ * pane fill, corner, edge, lens and drop, as gui props
7
+ * gap the gutter between two panes, and the width of the edge in it
8
+ */
9
+ export const pane = {
10
+ bg: 'var(--pane-fill, var(--sheet-1))',
11
+ rounded: 'var(--pane-round, var(--radius-xl))',
12
+ borderWidth: 1,
13
+ borderColor: 'var(--pane-edge, var(--white-08))',
14
+ backdropFilter: 'var(--pane-blur, blur(20px) saturate(1.8))',
15
+ boxShadow: 'var(--shadow-sheet-2)',
16
+ };
17
+ export const gap = 'var(--pane-gap, var(--space-2))';
package/lib/project.js CHANGED
@@ -51,6 +51,7 @@ import { Grip } from './grip.js';
51
51
  import { useHost, useTarget } from './host.js';
52
52
  import { MODES } from './landing.js';
53
53
  import { Out } from './out.js';
54
+ import { pane as cut } from './pane.js';
54
55
  import { Publish } from './publish.js';
55
56
  import { Attach, compose, Dictate, Files } from './tools.js';
56
57
  const LIVE = new Set(['running', 'paused']);
@@ -319,7 +320,7 @@ export function Project({ slug }) {
319
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.`
320
321
  : project
321
322
  ? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
322
- : `${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, { value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
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
323
324
  ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
324
325
  : 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] }));
325
326
  // The preview stays loaded while another view is open, so coming back to it
package/lib/run.js CHANGED
@@ -29,7 +29,7 @@ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from
29
29
  import { Steer } from '@hanzo/ui/agents';
30
30
  import { Composer } from '@hanzo/ui/chat';
31
31
  import { ChipSelect } from '@hanzo/ui/product';
32
- import { useEffect, useMemo, useState } from 'react';
32
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
33
33
  import { Access } from './access.js';
34
34
  import { approve, followUp, headline, retry, start } from './api/coding.js';
35
35
  import { ENSO, label as named, models } from './api/models.js';
@@ -38,14 +38,41 @@ import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
38
38
  import { verdict } from './api/verdict.js';
39
39
  import { useKept, useProjects, useRead, useRun } from './data.js';
40
40
  import { Desk } from './desk.js';
41
+ import { Grip } from './grip.js';
41
42
  import { useHost, useTarget } from './host.js';
42
43
  import { EFFORTS } from './landing.js';
43
44
  import { Out } from './out.js';
45
+ import { gap, pane } from './pane.js';
44
46
  import { usePrefs } from './prefs.js';
45
47
  import { Transcript } from './transcript.js';
46
48
  const LIVE = new Set(['running', 'paused', '']);
47
49
  // What this browser kept is read as it is found: a stored value that is not a string is none.
48
50
  const text = (v) => (typeof v === 'string' ? v : '');
51
+ /** The side pane's width, px: its default, the floor a drag holds, and what the transcript keeps beside it. */
52
+ const DESK_WIDTH = 480;
53
+ const DESK_FLOOR = 240;
54
+ const TALK_FLOOR = 300;
55
+ /** The most the side pane is drawn at before its row is measured, px. */
56
+ const DESK_CEIL = 960;
57
+ /**
58
+ * How wide an element is drawn, px, read before the first paint and again as it
59
+ * changes, so what depends on it is right on the first frame. 0 until it is drawn.
60
+ */
61
+ function useWidth() {
62
+ const [width, setWidth] = useState(0);
63
+ const [el, setEl] = useState(null);
64
+ useLayoutEffect(() => {
65
+ if (!el)
66
+ return;
67
+ setWidth(el.getBoundingClientRect().width);
68
+ if (typeof ResizeObserver === 'undefined')
69
+ return;
70
+ const watch = new ResizeObserver(([entry]) => setWidth(entry?.contentRect.width ?? 0));
71
+ watch.observe(el);
72
+ return () => watch.disconnect();
73
+ }, [el]);
74
+ return [width, (node) => setEl(node instanceof HTMLElement ? node : null)];
75
+ }
49
76
  export function Run({ id }) {
50
77
  const host = useHost();
51
78
  const t = useTarget();
@@ -57,6 +84,15 @@ export function Run({ id }) {
57
84
  const [busy, setBusy] = useState(false);
58
85
  const [notify, setNotify] = useState(false);
59
86
  const [desk, setDesk] = useKept('hanzo.build.desk', true);
87
+ // The side pane's width is the reader's, kept, and gives way so the
88
+ // transcript keeps its floor: bounded by the row it is drawn in once that is
89
+ // measured, and by its own `maxWidth` until then.
90
+ const [side, setSide] = useKept('hanzo.build.desk.span', DESK_WIDTH);
91
+ const [room, measure] = useWidth();
92
+ const most = room ? Math.max(DESK_FLOOR, room - TALK_FLOOR) : DESK_CEIL;
93
+ const across = Math.round(Math.min(most, Math.max(DESK_FLOOR, typeof side === 'number' ? side : DESK_WIDTH)));
94
+ // A drag repaints the pane's width directly and tells React once, on letting go.
95
+ const deskBox = useRef(null);
60
96
  // Stop was pressed here: the run keeps its work after it answers, and says so with its status.
61
97
  const [stopping, setStopping] = useState(false);
62
98
  const [naming, setNaming] = useState(false);
@@ -288,7 +324,7 @@ export function Run({ id }) {
288
324
  : 'Reading this run…';
289
325
  // What a new run from here is made with; words that steer this run use neither.
290
326
  const tuning = signed && !steering ? (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", justify: "flex-end", flexWrap: "wrap", rowGap: "$1", children: [_jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === model)?.label ?? named(model), chosen: catalog.value.find((m) => m.id === model) ?? null, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => choose({ ...chose, model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: pace.label, chosen: pace, items: EFFORTS, onChange: (e) => choose({ ...chose, effort: e.id }), placement: "top-end", width: 180 })] })) : undefined;
291
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
327
+ return (_jsxs(XStack, { ref: measure, "data-slot": "run-split", flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { "data-slot": "run", ...pane, flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
292
328
  // A paused run's agent was stopped where it stood; what it was running is cut off.
293
329
  live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve')) : undefined, approving: busy, onVerdict: (v) => verdict(t, id, record?.project ?? '', v), empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
294
330
  ? detail.error.message
@@ -314,7 +350,13 @@ export function Run({ id }) {
314
350
  : 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
315
351
  ...(paused ? ['pause'] : ['resume']),
316
352
  ...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
317
- ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', site: site, published: end.published, events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
353
+ ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, ...pane, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsxs(YStack, { "data-slot": "desk", ref: (el) => {
354
+ deskBox.current = el instanceof HTMLElement ? el : null;
355
+ }, ...pane,
356
+ // A pane of its own beside the run's, the gutter between them.
357
+ ml: gap, position: "relative", shrink: 0, minH: 0, display: "none", $md: { display: 'flex' }, style: { width: across, maxWidth: `calc(100% - ${TALK_FLOOR}px)`, minWidth: DESK_FLOOR }, children: [_jsx(Grip, { side: "right", span: across, floor: DESK_FLOOR, ceil: most, reset: Math.min(most, DESK_WIDTH), onSpan: (n) => deskBox.current?.style.setProperty('width', `${n}px`), onKeep: setSide, onShut: () => setDesk(false), label: "Resize the side pane",
358
+ // In the gutter between the two panes; `left` is measured inside the pane's 1px edge.
359
+ l: `calc(-1 * ${gap} - ${pane.borderWidth}px)`, width: gap }), _jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', site: site, published: end.published, events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })] })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
318
360
  if (e.key === 'Enter')
319
361
  save();
320
362
  } }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
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.",