@hanzo/build 0.2.29 → 0.2.31
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/api/coding.d.ts +40 -4
- package/lib/api/coding.js +59 -12
- package/lib/api/sandbox.d.ts +41 -8
- package/lib/api/sandbox.js +98 -9
- package/lib/api/turn.d.ts +47 -14
- package/lib/api/turn.js +129 -89
- package/lib/desk.d.ts +1 -1
- package/lib/desk.js +191 -19
- package/lib/door.d.ts +7 -3
- package/lib/door.js +18 -7
- package/lib/files.d.ts +4 -1
- package/lib/files.js +61 -13
- package/lib/out.d.ts +6 -0
- package/lib/out.js +20 -0
- package/lib/project.js +12 -7
- package/lib/run.js +19 -5
- package/lib/transcript.js +12 -24
- package/package.json +1 -1
- package/lib/api/harness.d.ts +0 -62
- package/lib/api/harness.js +0 -556
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
|
-
|
|
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 =
|
|
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 (!
|
|
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
|
|
6
|
-
*
|
|
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 {
|
|
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
|
-
|
|
20
|
-
const
|
|
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
|
-
|
|
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:
|
|
102
|
-
return (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: items.map((a, i) => {
|
|
103
|
-
|
|
104
|
-
|
|
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/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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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,7 @@ 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, start } from './api/coding.js';
|
|
33
|
+
import { approve, followUp, headline, retry, start } from './api/coding.js';
|
|
34
34
|
import { ENSO, label as named, models } from './api/models.js';
|
|
35
35
|
import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
|
|
36
36
|
import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
|
|
@@ -69,7 +69,13 @@ export function Run({ id }) {
|
|
|
69
69
|
const pace = EFFORTS.find((e) => e.id === chose?.effort) ?? EFFORTS.find((e) => e.id === prefs.code?.effort) ?? EFFORTS[1];
|
|
70
70
|
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
71
71
|
const mode = record?.mode ?? '';
|
|
72
|
-
|
|
72
|
+
// A run that narrated no ask of its own opens with the one its title records, once it has said anything.
|
|
73
|
+
const shown = useMemo(() => {
|
|
74
|
+
const drawn = cards(events, mode);
|
|
75
|
+
const ask = record ? headline(record.title, record.repo) : '';
|
|
76
|
+
const opens = drawn[0]?.kind === 'said' && drawn[0].who === 'person';
|
|
77
|
+
return ask && drawn.length && !opens ? [{ kind: 'said', key: 'ask', who: 'person', text: ask }, ...drawn] : drawn;
|
|
78
|
+
}, [events, mode, record]);
|
|
73
79
|
const end = outcome(events);
|
|
74
80
|
const plan = useMemo(() => steps(events), [events]);
|
|
75
81
|
const kept = useMemo(() => settled(events), [events]);
|
|
@@ -115,6 +121,10 @@ export function Run({ id }) {
|
|
|
115
121
|
}
|
|
116
122
|
: null;
|
|
117
123
|
const planned = mode === 'plan' && finished && state === 'done' ? answer(events) : '';
|
|
124
|
+
// A failed run is tried again with the person's words as it recorded them.
|
|
125
|
+
const failed = finished && state === 'error';
|
|
126
|
+
const asked = shown.find((c) => c.kind === 'said' && c.who === 'person');
|
|
127
|
+
const again = asked && asked.kind === 'said' ? asked.text : '';
|
|
118
128
|
// What the recorded read left out: it carries the latest fifty turns.
|
|
119
129
|
const hidden = Math.max(0, (detail.value?.events ?? 0) - (detail.value?.recent.length ?? 0));
|
|
120
130
|
// Only a run still working is on a step.
|
|
@@ -159,13 +169,17 @@ export function Run({ id }) {
|
|
|
159
169
|
};
|
|
160
170
|
/** 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. */
|
|
161
171
|
const tuned = (a) => ({ ...a, model: model === ENSO ? undefined : model, effort: pace.id });
|
|
162
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* A new run from this one's work, opened. A paused run it carries on is let go
|
|
174
|
+
* first: the new run works in the same sandbox, where nothing else may be at work.
|
|
175
|
+
*/
|
|
163
176
|
const carry = async (said, how = 'follow') => {
|
|
164
177
|
if (!earlier)
|
|
165
178
|
throw new Error('This run is still being read');
|
|
166
|
-
const run = await start(t, tuned(how === 'approve' ? approve(earlier, said) : followUp(earlier, said)));
|
|
167
179
|
if (paused)
|
|
168
180
|
await stop(t, id, 'Continued in a follow-up run').catch(() => undefined);
|
|
181
|
+
const ask = how === 'approve' ? approve(earlier, said) : how === 'retry' ? retry(earlier, said) : followUp(earlier, said);
|
|
182
|
+
const run = await start(t, tuned(ask));
|
|
169
183
|
setDraft('');
|
|
170
184
|
host.go(run.session);
|
|
171
185
|
return '';
|
|
@@ -283,7 +297,7 @@ export function Run({ id }) {
|
|
|
283
297
|
? setup
|
|
284
298
|
? 'Setting up environment'
|
|
285
299
|
: 'Starting…'
|
|
286
|
-
: 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$3", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || (signed && refused) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-label": `Steps · ${plan.length}`, "aria-expanded": unfolded, onPress: () => setUnfolded(!unfolded), px: "$3", py: "$1.5", gap: "$2", items: "center", hoverStyle: { bg: '$hover' }, children: [unfolded ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: `Steps · ${plan.length}` }), current && !unfolded ? (_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: { textAlign: 'right' }, children: current.name })) : null] }), unfolded
|
|
300
|
+
: 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$3", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || (signed && refused) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, failed ? (_jsx(XStack, { items: "center", gap: "$2", children: _jsx(Button, { size: "sm", disabled: busy, onPress: () => void act(() => carry(again, 'retry')), children: busy ? 'Starting…' : 'Try again' }) })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-label": `Steps · ${plan.length}`, "aria-expanded": unfolded, onPress: () => setUnfolded(!unfolded), px: "$3", py: "$1.5", gap: "$2", items: "center", hoverStyle: { bg: '$hover' }, children: [unfolded ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: `Steps · ${plan.length}` }), current && !unfolded ? (_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: { textAlign: 'right' }, children: current.name })) : null] }), unfolded
|
|
287
301
|
? plan.map((s) => {
|
|
288
302
|
const on = s === current;
|
|
289
303
|
return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || on ? '$ink' : '$soft', children: s.done ? '✓' : on ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
|
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
|
|
5
|
-
* output, the files it changed
|
|
6
|
-
*
|
|
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,46 +11,34 @@ 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
|
-
/**
|
|
26
|
-
|
|
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,
|
|
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:
|
|
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
|
-
return (_jsx(Step, { name: c.name, detail: c.detail, status: state(c.ran), "aria-label": `${c.name} ${c.detail}`.trim(), children: c.output.trim() ? (_jsx(Code, { language: "output", value: c.output, children: tail(c.output.replace(/\n+$/, '')) })) : undefined }, c.key));
|
|
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':
|
|
55
43
|
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: c.text }, c.key));
|
|
56
44
|
case 'plan':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
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.",
|
package/lib/api/harness.d.ts
DELETED
|
@@ -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
|
-
};
|