@hanzo/build 0.2.30 → 0.2.32

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/door.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * One door into a run's sandbox — its screen or its shell — framed from cloud's
4
- * own page with a fresh ticket in its address.
4
+ * own page with a fresh ticket in its address, while the sandbox runs. A
5
+ * suspended sandbox is resumed from the pane's bar, never by looking at it.
5
6
  *
6
7
  * The page says when its socket is up. A page that says it failed, or silence
7
8
  * past the deadline, is a failed connection, and Retry mints a new ticket: a
@@ -17,14 +18,18 @@ import { useTarget } from './host.js';
17
18
  const SOURCE = { screen: 'hanzo-screen', terminal: 'hanzo-term' };
18
19
  const WHAT = { screen: 'desktop', terminal: 'terminal' };
19
20
  const DEADLINE = 20_000;
20
- export function Door({ which, sandbox, live, session }) {
21
+ /**
22
+ * `held` is where the run's sandbox is in its life, '' until it has been read. Only
23
+ * a running sandbox has an open door.
24
+ */
25
+ export function Door({ which, sandbox, held, session }) {
21
26
  const t = useTarget();
22
27
  const frame = useRef(null);
23
28
  const [src, setSrc] = useState('');
24
29
  const [state, setState] = useState('opening');
25
30
  const [why, setWhy] = useState('');
26
31
  const [attempt, setAttempt] = useState(0);
27
- const up = live && Boolean(sandbox);
32
+ const up = Boolean(sandbox) && held === 'running';
28
33
  useEffect(() => {
29
34
  if (!up)
30
35
  return;
@@ -73,12 +78,18 @@ export function Door({ which, sandbox, live, session }) {
73
78
  clearTimeout(late);
74
79
  };
75
80
  }, [src, which]);
76
- if (!live) {
77
- return _jsx(Shut, { title: "Not running", body: `This run’s ${WHAT[which]} closed when the run stopped.` });
78
- }
79
- if (!sandbox) {
81
+ if (!sandbox || held === '' || held === 'pending') {
80
82
  return _jsx(Shut, { title: "Starting", body: `The ${WHAT[which]} opens once the run’s sandbox is up.` });
81
83
  }
84
+ if (held === 'parked') {
85
+ return _jsx(Shut, { title: "Suspended", body: `This run’s sandbox is suspended with its files kept. Resume it from the bar above to open its ${WHAT[which]}.` });
86
+ }
87
+ if (held === 'gone') {
88
+ return _jsx(Shut, { title: "Ended", body: "This run\u2019s sandbox has ended. Its work is on its branch and in its artifacts." });
89
+ }
90
+ if (held === 'error') {
91
+ return _jsx(Shut, { title: "Unavailable", body: `This run’s sandbox could not start, so it has no ${WHAT[which]}.` });
92
+ }
82
93
  if (state === 'failed') {
83
94
  return (_jsx(Shut, { title: "Failed to connect", body: why || `Unable to reach the ${WHAT[which]}. The run may have stopped.`, children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: "Retry" }) }));
84
95
  }
package/lib/files.d.ts CHANGED
@@ -5,7 +5,10 @@ export declare function Files({ session, repo, branch, sandbox, live, }: {
5
5
  sandbox: string;
6
6
  live: boolean;
7
7
  }): import("react").JSX.Element;
8
- export declare function Artifacts({ repo, branch, mode, project, site, pr, onEnvironment, }: {
8
+ export declare function Artifacts({ session, sandbox, repo, branch, mode, project, site, pr, onEnvironment, }: {
9
+ session: string;
10
+ /** The run's sandbox, which serves its previews while it is kept. */
11
+ sandbox: string;
9
12
  repo: string;
10
13
  branch: string;
11
14
  mode: string;
package/lib/files.js CHANGED
@@ -2,22 +2,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * A run's Files tab: its working tree, live from the sandbox while the run holds
4
4
  * one — every edit the agent has made, committed or not — and its branch on the
5
- * forge otherwise. And its Artifacts tab: what the run produced — its published
6
- * site, its pull request, its branch, its environment proposal and its project.
5
+ * forge otherwise. And its Artifacts tab: what the run left — every file it added
6
+ * or changed and the whole change as a patch, kept after its sandbox is gone; its
7
+ * previews, pull request and deployments — then its published site, its branch,
8
+ * its environment proposal and its project.
7
9
  */
8
10
  import { SizableText, XStack, YStack } from '@hanzo/gui';
9
- import { ExternalLink, File, Folder, Globe, GitBranch, GitPullRequest, Layers, Settings2 } from '@hanzogui/lucide-icons-2';
11
+ import { Download, ExternalLink, File, FileDiff, Folder, Globe, GitBranch, GitPullRequest, Layers, MonitorPlay, Rocket, Settings2 } from '@hanzogui/lucide-icons-2';
10
12
  import { useState } from 'react';
11
13
  import { blob, tree } from './api/changes.js';
12
- import { read as look } from './api/sandbox.js';
14
+ import { artifact, artifacts } from './api/coding.js';
15
+ import { preview, read as look } from './api/sandbox.js';
13
16
  import { useRead } from './data.js';
14
17
  import { useHost, useTarget } from './host.js';
15
- import { Out } from './out.js';
18
+ import { away, Out } from './out.js';
16
19
  const monoWrap = { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
17
20
  export function Files({ session, repo, branch, sandbox, live, }) {
18
21
  const reachable = live && Boolean(sandbox);
19
- const [source, setSource] = useState(reachable ? 'live' : 'branch');
20
- const from = reachable ? source : 'branch';
22
+ // Live once the sandbox is reachable, unless the branch was picked.
23
+ const [source, setSource] = useState(null);
24
+ const from = reachable ? (source ?? 'live') : 'branch';
21
25
  return (_jsxs(YStack, { gap: "$3", children: [reachable ? (_jsxs(XStack, { gap: "$1", items: "center", flexWrap: "wrap", children: [_jsx(Pick, { on: from === 'live', onPress: () => setSource('live'), children: "Live" }), _jsx(Pick, { on: from === 'branch', onPress: () => setSource('branch'), children: "Branch" })] })) : null, from === 'live' ? _jsx(Live, { sandbox: sandbox }) : _jsx(Branch, { session: session, label: `${repo}${branch ? `@${branch}` : ''}` })] }));
22
26
  }
23
27
  /** The sandbox's working tree, as it is right now. */
@@ -84,9 +88,53 @@ function Branch({ session, label }) {
84
88
  function Listing({ where, up, error, loading, rows, file, body, }) {
85
89
  return (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", children: [up ? (_jsx(XStack, { render: "button", "aria-label": "Up one directory", onPress: up, px: "$1", py: "$1", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Up" }) })) : null, _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: where })] }), error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: error })) : loading ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading\u2026" })) : rows.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "This directory is empty." })) : (rows.map((r) => (_jsxs(XStack, { render: "button", "aria-label": r.dir === false ? `Read ${r.name}` : `Open ${r.name}`, onPress: r.onPress, items: "center", gap: "$2", py: "$1", hoverStyle: { bg: '$hover' }, children: [r.dir === false ? _jsx(File, { size: 14 }) : r.dir ? _jsx(Folder, { size: 14 }) : _jsx(File, { size: 14, opacity: 0.6 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: r.name })] }, r.key)))), file && body ? (_jsxs(YStack, { gap: "$1", pt: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: file }), _jsx(SizableText, { size: "$1", color: "$ink", style: monoWrap, children: body })] })) : null] }));
86
90
  }
87
- export function Artifacts({ repo, branch, mode, project, site, pr, onEnvironment, }) {
91
+ /** A stored artifact's size, as a person reads it. */
92
+ const size = (n) => (n < 1024 ? `${n} B` : n < 1 << 20 ? `${(n / 1024).toFixed(1)} KB` : `${(n / (1 << 20)).toFixed(1)} MB`);
93
+ /** What each kind of artifact is drawn with, and says it is. */
94
+ const KIND = {
95
+ file: { icon: _jsx(File, { size: 16 }), note: (a) => `Changed file · ${size(a.size)}` },
96
+ patch: { icon: _jsx(FileDiff, { size: 16 }), note: (a) => `The whole change as a patch · ${size(a.size)}` },
97
+ preview: { icon: _jsx(MonitorPlay, { size: 16 }), note: (a) => `What the run served on port ${a.port}` },
98
+ pull: { icon: _jsx(GitPullRequest, { size: 16 }), note: () => 'Pull request' },
99
+ deploy: { icon: _jsx(Rocket, { size: 16 }), note: () => 'Deployment' },
100
+ };
101
+ export function Artifacts({ session, sandbox, repo, branch, mode, project, site, pr, onEnvironment, }) {
88
102
  const host = useHost();
103
+ const t = useTarget();
104
+ const left = useRead(host.person ? () => artifacts(t, session) : null, { saved: '', artifacts: [] }, [t, session, host.person]);
105
+ const [note, setNote] = useState('');
106
+ // Bytes come with the person's bearer, so a stored artifact is fetched, then saved.
107
+ const save = async (a) => {
108
+ try {
109
+ const url = URL.createObjectURL(await artifact(t, session, a.name));
110
+ const link = document.createElement('a');
111
+ link.href = url;
112
+ link.download = a.name.split('/').pop() || a.name;
113
+ link.click();
114
+ setTimeout(() => URL.revokeObjectURL(url), 10_000);
115
+ }
116
+ catch (e) {
117
+ setNote(e.message);
118
+ }
119
+ };
120
+ // A preview is opened with a fresh ticket, in a tab of its own.
121
+ const open = (a) => away(preview(t, sandbox, a.port)).catch((e) => setNote(e.message));
89
122
  const items = [];
123
+ for (const a of left.value.artifacts) {
124
+ const stored = a.kind === 'file' || a.kind === 'patch';
125
+ const link = a.kind === 'pull' || a.kind === 'deploy';
126
+ if ((a.kind === 'preview' && !sandbox) || (link && !/^https:\/\//.test(a.url)))
127
+ continue;
128
+ items.push({
129
+ key: `${a.kind}:${a.name}`,
130
+ icon: KIND[a.kind].icon,
131
+ title: link ? a.url.replace(/^https:\/\//, '') : a.name,
132
+ note: KIND[a.kind].note(a),
133
+ href: link ? a.url : undefined,
134
+ onPress: stored ? () => void save(a) : a.kind === 'preview' ? () => void open(a) : undefined,
135
+ action: stored ? _jsx(Download, { size: 14 }) : a.kind === 'preview' ? _jsx(ExternalLink, { size: 14 }) : undefined,
136
+ });
137
+ }
90
138
  if (site)
91
139
  items.push({ key: 'site', icon: _jsx(Globe, { size: 16 }), title: site.replace(/^https:\/\//, ''), note: 'Where this run is published', href: site });
92
140
  if (pr.href)
@@ -98,11 +146,11 @@ export function Artifacts({ repo, branch, mode, project, site, pr, onEnvironment
98
146
  if (project)
99
147
  items.push({ key: 'project', icon: _jsx(Layers, { size: 16 }), title: project, note: 'The project this run builds', onPress: () => host.go(project) });
100
148
  if (!items.length)
101
- return _jsx(Soft, { children: "This run has produced no artifacts yet." });
102
- return (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: items.map((a, i) => {
103
- const row = (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: a.href || a.onPress ? { bg: '$hover' } : undefined, children: [a.icon, _jsxs(YStack, { flex: 1, minW: 0, style: { textAlign: 'left' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: a.title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: a.note })] }), a.href ? _jsx(ExternalLink, { size: 14 }) : null] }));
104
- return a.href ? (_jsx(Out, { href: a.href, label: a.title, children: _jsx(YStack, { width: "100%", children: row }) }, a.key)) : a.onPress ? (_jsx(YStack, { render: "button", "aria-label": a.title, onPress: a.onPress, children: row }, a.key)) : (_jsx(YStack, { children: row }, a.key));
105
- }) }));
149
+ return _jsx(Soft, { children: left.error?.message || (left.loading ? 'Reading what this run left…' : 'This run has produced no artifacts yet.') });
150
+ return (_jsxs(YStack, { gap: "$2", children: [note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: items.map((a, i) => {
151
+ const row = (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: a.href || a.onPress ? { bg: '$hover' } : undefined, children: [a.icon, _jsxs(YStack, { flex: 1, minW: 0, style: { textAlign: 'left' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: a.title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: a.note })] }), a.action ?? (a.href ? _jsx(ExternalLink, { size: 14 }) : null)] }));
152
+ return a.href ? (_jsx(Out, { href: a.href, label: a.title, children: _jsx(YStack, { width: "100%", children: row }) }, a.key)) : a.onPress ? (_jsx(YStack, { render: "button", "aria-label": a.title, onPress: a.onPress, children: row }, a.key)) : (_jsx(YStack, { children: row }, a.key));
153
+ }) })] }));
106
154
  }
107
155
  function Pick({ on, onPress, children }) {
108
156
  return (_jsx(XStack, { render: "button", "aria-label": children, onPress: onPress, px: "$2.5", py: "$1", rounded: "$2", bg: on ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', children: children }) }));
package/lib/landing.js CHANGED
@@ -23,6 +23,7 @@ import { start, unhonoured } from './api/coding.js';
23
23
  import { asRepo, chosen, codebases, one } from './api/codebases.js';
24
24
  import { read, SETUP } from './api/environment.js';
25
25
  import { SetupDialog } from './environment.js';
26
+ import { Access } from './access.js';
26
27
  import { ENSO, models } from './api/models.js';
27
28
  import { ready, SANDBOX } from './api/places.js';
28
29
  import { isForge } from './choice.js';
@@ -207,7 +208,7 @@ export function Landing({ onStarted }) {
207
208
  });
208
209
  },
209
210
  }, placeholder: "New project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
210
- const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
211
+ 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 })] }));
211
212
  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'
212
213
  ? `A proposed environment for ${codebase} is waiting for review.`
213
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: () => {
package/lib/out.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * A link out of the builder: a new tab, no handle back to this page, no
3
3
  * referrer. Only ever handed an https address the caller has already checked.
4
+ * `away` is the same for an address that has to be minted first.
4
5
  */
5
6
  import type { ReactNode } from 'react';
6
7
  export declare function Out({ href, label, children }: {
@@ -8,3 +9,8 @@ export declare function Out({ href, label, children }: {
8
9
  label?: string;
9
10
  children: ReactNode;
10
11
  }): import("react").JSX.Element;
12
+ /**
13
+ * Opens a tab for an address that takes a moment to mint: the tab opens now, while
14
+ * the press still counts, and goes to the address once it comes. A refusal closes it.
15
+ */
16
+ export declare function away(url: Promise<string>): Promise<void>;
package/lib/out.js CHANGED
@@ -2,3 +2,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  export function Out({ href, label, children }) {
3
3
  return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", "aria-label": label, style: { color: 'inherit', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6 }, children: children }));
4
4
  }
5
+ /**
6
+ * Opens a tab for an address that takes a moment to mint: the tab opens now, while
7
+ * the press still counts, and goes to the address once it comes. A refusal closes it.
8
+ */
9
+ export async function away(url) {
10
+ const w = window.open('about:blank', '_blank');
11
+ if (w)
12
+ w.opener = null;
13
+ try {
14
+ const to = await url;
15
+ if (w)
16
+ w.location.replace(to);
17
+ else
18
+ window.open(to, '_blank', 'noopener');
19
+ }
20
+ catch (e) {
21
+ w?.close();
22
+ throw e;
23
+ }
24
+ }
package/lib/project.js CHANGED
@@ -43,7 +43,7 @@ import { start } from './api/coding.js';
43
43
  import { blob, tree } from './api/git.js';
44
44
  import { deployments, name as repoName, ours, templates } from './api/projects.js';
45
45
  import { list, message, stop } from './api/sessions.js';
46
- import { outcome, pull, said, who } from './api/turn.js';
46
+ import { decode, outcome, pull, said, who } from './api/turn.js';
47
47
  import { verdict as record } from './api/verdict.js';
48
48
  import { useKept, useProjects, useRead, useRun } from './data.js';
49
49
  import { Merge } from './git.js';
@@ -213,12 +213,17 @@ export function Project({ slug }) {
213
213
  const lines = useMemo(() => [
214
214
  ...run.events
215
215
  .filter((e) => e.kind === 'log' || e.kind === 'tool-call' || e.kind === 'status')
216
- .map((e) => ({
217
- id: `r${e.seq || e.id}`,
218
- level: e.kind === 'status' && /error/.test(said(e, run.record?.mode).toLowerCase()) ? 'error' : e.kind === 'tool-call' ? 'info' : 'log',
219
- text: said(e, run.record?.mode),
220
- source: e.kind === 'status' ? 'run' : who(e.actor),
221
- }))
216
+ .map((e) => {
217
+ // The console is where a run's error is read whole; the chat says it in one plain line.
218
+ const body = decode(e.payload);
219
+ const error = e.kind === 'status' && body && typeof body === 'object' && typeof body.error === 'string' ? body.error : '';
220
+ return {
221
+ id: `r${e.seq || e.id}`,
222
+ level: error || (e.kind === 'status' && /error/.test(said(e, run.record?.mode).toLowerCase())) ? 'error' : e.kind === 'tool-call' ? 'info' : 'log',
223
+ text: error || said(e, run.record?.mode),
224
+ source: e.kind === 'status' ? 'run' : who(e.actor),
225
+ };
226
+ })
222
227
  .filter((l) => l.text),
223
228
  ...pageLines,
224
229
  ], [run.events, run.record?.mode, pageLines]);
package/lib/run.js CHANGED
@@ -30,7 +30,8 @@ import { Steer } from '@hanzo/ui/agents';
30
30
  import { Composer } from '@hanzo/ui/chat';
31
31
  import { ChipSelect } from '@hanzo/ui/product';
32
32
  import { useEffect, useMemo, useState } from 'react';
33
- import { approve, followUp, retry, start } from './api/coding.js';
33
+ import { Access } from './access.js';
34
+ import { approve, followUp, headline, retry, start } from './api/coding.js';
34
35
  import { ENSO, label as named, models } from './api/models.js';
35
36
  import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
36
37
  import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
@@ -69,7 +70,13 @@ export function Run({ id }) {
69
70
  const pace = EFFORTS.find((e) => e.id === chose?.effort) ?? EFFORTS.find((e) => e.id === prefs.code?.effort) ?? EFFORTS[1];
70
71
  const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
71
72
  const mode = record?.mode ?? '';
72
- const shown = useMemo(() => cards(events, mode), [events, mode]);
73
+ // A run that narrated no ask of its own opens with the one its title records, once it has said anything.
74
+ const shown = useMemo(() => {
75
+ const drawn = cards(events, mode);
76
+ const ask = record ? headline(record.title, record.repo) : '';
77
+ const opens = drawn[0]?.kind === 'said' && drawn[0].who === 'person';
78
+ return ask && drawn.length && !opens ? [{ kind: 'said', key: 'ask', who: 'person', text: ask }, ...drawn] : drawn;
79
+ }, [events, mode, record]);
73
80
  const end = outcome(events);
74
81
  const plan = useMemo(() => steps(events), [events]);
75
82
  const kept = useMemo(() => settled(events), [events]);
@@ -163,14 +170,17 @@ export function Run({ id }) {
163
170
  };
164
171
  /** A new run's ask with the model and effort the foot shows. The router is the platform's default, so it is not named. */
165
172
  const tuned = (a) => ({ ...a, model: model === ENSO ? undefined : model, effort: pace.id });
166
- /** A new run from this one's work, opened. A paused run it carries on is let go once the new one is admitted. */
173
+ /**
174
+ * A new run from this one's work, opened. A paused run it carries on is let go
175
+ * first: the new run works in the same sandbox, where nothing else may be at work.
176
+ */
167
177
  const carry = async (said, how = 'follow') => {
168
178
  if (!earlier)
169
179
  throw new Error('This run is still being read');
170
- const ask = how === 'approve' ? approve(earlier, said) : how === 'retry' ? retry(earlier, said) : followUp(earlier, said);
171
- const run = await start(t, tuned(ask));
172
180
  if (paused)
173
181
  await stop(t, id, 'Continued in a follow-up run').catch(() => undefined);
182
+ const ask = how === 'approve' ? approve(earlier, said) : how === 'retry' ? retry(earlier, said) : followUp(earlier, said);
183
+ const run = await start(t, tuned(ask));
174
184
  setDraft('');
175
185
  host.go(run.session);
176
186
  return '';
@@ -277,7 +287,7 @@ export function Run({ id }) {
277
287
  ? 'Follow up — continues in a new run'
278
288
  : 'Reading this run…';
279
289
  // What a new run from here is made with; words that steer this run use neither.
280
- 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, 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;
290
+ 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;
281
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,
282
292
  // A paused run's agent was stopped where it stood; what it was running is cut off.
283
293
  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
@@ -10,6 +10,7 @@ import { ChevronRight } from '@hanzogui/lucide-icons-2';
10
10
  import { ChipSelect } from '@hanzo/ui/product';
11
11
  import { useState } from 'react';
12
12
  import { unhonoured } from '../api/coding.js';
13
+ import { Access } from '../access.js';
13
14
  import { ENSO, models } from '../api/models.js';
14
15
  import { SANDBOX } from '../api/places.js';
15
16
  import { usePlaces, useRead } from '../data.js';
@@ -47,7 +48,7 @@ export function Code() {
47
48
  const go = (section) => host.go(path({ kind: 'settings', section }));
48
49
  const effort = EFFORTS.find((e) => e.id === now.effort);
49
50
  const mode = MODES.find((m) => m.id === now.mode);
50
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Code", detail: "How a new run starts. New begins from these until you choose otherwise there." }), _jsxs(Group, { title: "Defaults", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Model", detail: "Enso picks one for each step; any other model runs them all.", trailing: _jsx(ChipSelect, { name: "Default model", label: model.label, chosen: model, items: catalog.value, onChange: (m) => keep({ model: m.id }), placeholder: "Search models\u2026", placement: "bottom-end", loading: catalog.loading, error: catalog.error?.message ?? null }) }), _jsx(Row, { title: "Effort", detail: "How long the agent thinks before it acts.", trailing: _jsx(ChipSelect, { name: "Default effort", label: effort.label, chosen: effort, items: EFFORTS, onChange: (e) => keep({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: "Mode", detail: mode.hint, trailing: _jsx(ChipSelect, { name: "Default mode", label: mode.label, chosen: mode, items: MODES, onChange: (m) => keep({ mode: m.id }), placement: "bottom-end", width: 260 }) }), _jsx(Row, { title: "Where it runs", detail: "The Hanzo sandbox, or one of your organization\u2019s machines.", trailing: _jsx(ChipSelect, { name: "Default place", label: place.label, chosen: place, items: where, onChange: (w) => keep({ place: w.place }), placeholder: "Search machines\u2026", placement: "bottom-end", loading: places.loading, error: places.error?.message ?? null }) })] }), _jsx(Note, { children: unhonoured(now.mode, now.place) })] }), _jsx(Group, { title: "Workspace", children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Environments", detail: "What a run installs and starts in each codebase, and the secrets it exports.", trailing: _jsx(Link, { label: "Environments", onPress: () => go('environments') }) }), _jsx(Row, { title: "Machines", detail: "Your organization\u2019s own computers that take runs.", trailing: _jsx(Link, { label: "Machines", onPress: () => go('machines') }) })] }) }), _jsx(Note, { children: note })] }));
51
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Code", detail: "How a new run starts. New begins from these until you choose otherwise there." }), _jsxs(Group, { title: "Defaults", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Model", detail: "Enso picks one for each step; any other model runs them all.", trailing: _jsx(ChipSelect, { name: "Default model", label: model.label, chosen: model, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => keep({ model: m.id }), placeholder: "Search models\u2026", placement: "bottom-end", loading: catalog.loading, error: catalog.error?.message ?? null }) }), _jsx(Row, { title: "Effort", detail: "How long the agent thinks before it acts.", trailing: _jsx(ChipSelect, { name: "Default effort", label: effort.label, chosen: effort, items: EFFORTS, onChange: (e) => keep({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: "Mode", detail: mode.hint, trailing: _jsx(ChipSelect, { name: "Default mode", label: mode.label, chosen: mode, items: MODES, onChange: (m) => keep({ mode: m.id }), placement: "bottom-end", width: 260 }) }), _jsx(Row, { title: "Where it runs", detail: "The Hanzo sandbox, or one of your organization\u2019s machines.", trailing: _jsx(ChipSelect, { name: "Default place", label: place.label, chosen: place, items: where, onChange: (w) => keep({ place: w.place }), placeholder: "Search machines\u2026", placement: "bottom-end", loading: places.loading, error: places.error?.message ?? null }) })] }), _jsx(Note, { children: unhonoured(now.mode, now.place) })] }), _jsx(Group, { title: "Workspace", children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Environments", detail: "What a run installs and starts in each codebase, and the secrets it exports.", trailing: _jsx(Link, { label: "Environments", onPress: () => go('environments') }) }), _jsx(Row, { title: "Machines", detail: "Your organization\u2019s own computers that take runs.", trailing: _jsx(Link, { label: "Machines", onPress: () => go('machines') }) })] }) }), _jsx(Note, { children: note })] }));
51
52
  }
52
53
  function Link({ label, onPress }) {
53
54
  return (_jsx(YStack, { render: "button", "aria-label": `Open ${label}`, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onPress, children: _jsx(ChevronRight, { size: 16 }) }));
package/lib/transcript.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * A run's transcript, drawn by what each part is (turn.ts `cards`): what the
4
- * agent said as markdown, each command it ran as a card that opens onto its
5
- * output, the files it changed with their diff, the files it read as chips,
6
- * the run's own steps, and a plan run's plan with the button that builds it.
4
+ * agent said as markdown, its reasoning folded, each command it ran as a card
5
+ * that opens onto its output, the files it changed, its plan as a checklist, the
6
+ * run's own steps, and a plan run's plan with the button that builds it.
7
7
  * What the agent said, and its plan, can be copied and judged, as a reply on
8
8
  * claude.ai can: a thumb pressed again takes the verdict back.
9
9
  *
@@ -11,44 +11,32 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * scrolls up (Thread). Everything drawn is text.
12
12
  */
13
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
14
- import { FileText } from '@hanzogui/lucide-icons-2';
15
14
  import { Button } from '@hanzo/ui';
16
15
  import { Feedback } from '@hanzo/ui/agents';
17
16
  import { Code, Message, Step, Thread } from '@hanzo/ui/chat';
18
17
  import { useState } from 'react';
19
- import { Patch } from './git.js';
20
18
  import { Prose } from './prose.js';
21
19
  /** A card's output, its tail: what a command said last is why it stopped. */
22
20
  const TAIL = 6000;
23
21
  const tail = (s) => (s.length > TAIL ? `…${s.slice(-TAIL)}` : s);
24
22
  const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
25
- /** Consecutive reads draw as one row of chips. */
26
- function rows(cards) {
27
- const out = [];
28
- for (const c of cards) {
29
- const last = out[out.length - 1];
30
- if (c.kind === 'read' && Array.isArray(last))
31
- last.push(c);
32
- else
33
- out.push(c.kind === 'read' ? [c] : c);
34
- }
35
- return out;
36
- }
23
+ /** What a thought is about: its first line, without the markdown that bolds it. */
24
+ const firstLine = (text) => text.trim().split('\n')[0].replace(/[*_#`]/g, '').slice(0, 120);
37
25
  export function Transcript({ cards, live, empty, header, onApprove, approving = false, onVerdict, }) {
38
26
  // A card the run never finished is cut off once the run has ended, not still going.
39
27
  const state = (ran) => (ran === 'running' && !live ? 'cancelled' : ran);
40
- return (_jsxs(Thread, { maxWidth: 0, gap: 14, column: { px: 0 }, "aria-label": "Transcript", children: [header, cards.length === 0 ? _jsx(YStack, { py: "$2", children: empty }) : null, rows(cards).map((row) => {
41
- if (Array.isArray(row)) {
42
- return (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", children: row.map((r) => (_jsxs(XStack, { "aria-label": `Read ${r.file}`, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", maxW: "100%", children: [_jsx(FileText, { size: 12, color: "$soft" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Read" }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, style: mono, children: r.file })] }, r.key))) }, row[0].key));
43
- }
44
- const c = row;
28
+ return (_jsxs(Thread, { maxWidth: 0, gap: 14, column: { px: 0 }, "aria-label": "Transcript", children: [header, cards.length === 0 ? _jsx(YStack, { py: "$2", children: empty }) : null, cards.map((c) => {
45
29
  switch (c.kind) {
46
30
  case 'said':
47
31
  return c.who === 'person' ? (_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$3", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: c.text }) }, c.key)) : (_jsx(Message, { role: "assistant", actions: _jsx(Judge, { text: c.text, onVerdict: onVerdict }), children: _jsx(Prose, { text: c.text }) }, c.key));
32
+ case 'think':
33
+ return (_jsx(Step, { name: "Thought", detail: firstLine(c.text), status: "done", "aria-label": "Thought", children: _jsx(Prose, { text: c.text }) }, c.key));
34
+ case 'todo':
35
+ return (_jsx(YStack, { gap: "$1", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", "aria-label": "Plan checklist", children: c.items.map((x, i) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: x.done ? '$ink' : '$soft', children: x.done ? '✓' : '○' }), _jsx(SizableText, { flex: 1, size: "$2", color: x.done ? '$soft' : '$ink', children: x.text })] }, i))) }, c.key));
48
36
  case 'shell':
49
37
  return (_jsx(Step, { name: "Shell", detail: c.command, status: state(c.ran), "aria-label": `Command ${c.command}`, children: _jsx(Code, { language: "shell", value: c.output ? `$ ${c.command}\n${c.output}` : `$ ${c.command}`, children: c.output ? `$ ${c.command}\n${tail(c.output)}` : `$ ${c.command}` }) }, c.key));
50
38
  case 'edit':
51
- return (_jsx(Step, { name: c.files.length === 1 ? 'Edited' : c.files.length ? `Edited ${c.files.length} files` : 'Edited files', detail: c.files.join(', '), status: state(c.ran), "aria-label": `Edited ${c.files.join(', ') || 'files'}`, children: c.patch ? (_jsx(YStack, { overflow: "scroll", maxH: 480, children: _jsx(Patch, { text: c.patch }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: c.files.length ? c.files.join('\n') : 'The harness named no files.' })) }, c.key));
39
+ return (_jsx(Step, { name: c.files.length === 1 ? 'Edited' : c.files.length ? `Edited ${c.files.length} files` : 'Edited files', detail: c.files.join(', '), status: state(c.ran), "aria-label": `Edited ${c.files.join(', ') || 'files'}`, children: _jsx(SizableText, { size: "$1", color: "$soft", children: c.files.length ? `${c.files.join('\n')}\n\nThe diff is in the run’s Git tab.` : 'The agent named no files.' }) }, c.key));
52
40
  case 'step':
53
41
  return (_jsx(Step, { name: c.name, detail: c.detail, status: state(c.ran), defaultOpen: c.name === 'Error', "aria-label": `${c.name} ${c.detail}`.trim(), children: c.name === 'Error' ? (_jsx(SizableText, { size: "$2", color: "$ink", style: { ...mono, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: c.output })) : c.output.trim() ? (_jsx(Code, { language: "output", value: c.output, children: tail(c.output.replace(/\n+$/, '')) })) : undefined }, c.key));
54
42
  case 'note':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
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.",
@@ -11,7 +11,7 @@
11
11
  "@hanzo/font": "1.9.3",
12
12
  "@hanzo/gui": "8.3.5",
13
13
  "@hanzo/iam": "0.21.21",
14
- "@hanzo/ui": "8.27.16",
14
+ "@hanzo/ui": "8.27.22",
15
15
  "@hanzo/vite": "0.1.4",
16
16
  "@hanzo/voice": "0.1.8",
17
17
  "@hanzogui/lucide-icons-2": "8.3.5",
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@hanzo/gui": ">=8.3.5",
59
- "@hanzo/ui": ">=8.27.16",
59
+ "@hanzo/ui": ">=8.27.22",
60
60
  "@hanzogui/lucide-icons-2": ">=8.3.5",
61
61
  "react": ">=19.2.8",
62
62
  "@hanzo/voice": ">=0.1.8"
@@ -1,62 +0,0 @@
1
- /**
2
- * The agent's own output, read as cards.
3
- *
4
- * A sandbox run's agent is `dev exec`, and everything it prints reaches the
5
- * run's session as `log` chunks (apps/sandbox work.go). It prints each item of
6
- * its turn under a header line (hanzo dev, exec/src/event_processor_with_human_output.rs):
7
- *
8
- * user / <task> what the agent was asked
9
- * exec / <command> in <dir> a command started
10
- * succeeded in 12ms: / <output> …how it ended, then what it printed
11
- * codex / <text> the agent said something
12
- * apply patch / patch: completed / <paths>, then the turn's diff
13
- * mcp: server/tool (completed) a tool call
14
- * web search: <query>
15
- * ✓ step / → step / • step its plan
16
- * tokens used / <n> the end; the final message follows once more, bare
17
- *
18
- * The sandbox's older harness (dev 0.6.94) prints another grammar: it stamps
19
- * each item `[2026-09-29T16:07:35] ` and spells its headers its own way —
20
- * `User instructions:`, `dev`, `thinking`, `exec <command> in <dir>`,
21
- * `<command> succeeded in 1m 05s:`, `tool <call>`, `🌐 Search: <query>`,
22
- * `apply_patch auto_approved=true:`, `turn diff:`, `tokens used: <n>`. A stamped
23
- * line starts an item and says where the unstamped lines after it belong
24
- * (`stamped`). Every stamped line no header owns is the harness's diagnostics.
25
- *
26
- * Warnings, errors, retries and diagnostics are the agent's log, one collapsed
27
- * step, never the conversation; token counts are dropped.
28
- *
29
- * The chunks are cut by a clock, not by line, so a line is read only once it
30
- * is whole. Lines no header owns — reasoning, or the tail of output from before
31
- * the recorded events begin — are prose. Everything here becomes TEXT on the
32
- * screen; nothing is markup.
33
- *
34
- * The task under `user` is what the person asked, with what the platform told
35
- * the agent ahead of it and what a follow-up said after it taken off (`asked`),
36
- * so it opens the transcript as the person's own words.
37
- */
38
- import type { Card } from './turn.ts';
39
- /**
40
- * A shell command's words, as `sh` splits them: quotes grouped, `'"'"'`
41
- * joined, a backslash escaping the next character outside single quotes.
42
- */
43
- export declare function words(line: string): string[];
44
- /** The command a harness ran, out of the `bash -lc '…'` it wraps each one in. */
45
- export declare function unwrap(command: string): string;
46
- /**
47
- * The file a command only reads, or ''. `cat f`, `nl -ba f`, `sed -n '1,80p' f`,
48
- * `head -n 40 f`, `tail f` — one file, and nothing piped, chained or redirected.
49
- */
50
- export declare function reads(command: string): string;
51
- /** A path to show: a relative one as it is, an absolute one by its name — never the sandbox's layout. */
52
- export declare const shown: (path: string) => string;
53
- /** What the person asked, out of the task the agent was given. A follow-up with nothing said keeps its own line. */
54
- export declare function asked(task: string): string;
55
- /** A unified diff's sections, by the path each changes, from its first hunk on. */
56
- export declare function sections(diff: string): Map<string, string>;
57
- /** A reader that appends the cards it finds to `out`, keyed by `mint`. */
58
- export declare function harness(out: Card[], mint: () => string): {
59
- feed(chunk: string): void;
60
- /** The last line even without its newline, and what only the whole output can decide. */
61
- end(): void;
62
- };