@hanzo/build 0.2.28 → 0.2.29
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/harness.d.ts +11 -0
- package/lib/api/harness.js +182 -2
- package/lib/api/turn.d.ts +5 -0
- package/lib/api/turn.js +17 -1
- package/lib/desk.d.ts +5 -1
- package/lib/desk.js +22 -11
- package/lib/files.d.ts +7 -1
- package/lib/files.js +11 -10
- package/lib/run.js +11 -2
- package/package.json +1 -1
package/lib/api/harness.d.ts
CHANGED
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
* ✓ step / → step / • step its plan
|
|
16
16
|
* tokens used / <n> the end; the final message follows once more, bare
|
|
17
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
|
+
*
|
|
18
29
|
* The chunks are cut by a clock, not by line, so a line is read only once it
|
|
19
30
|
* is whole. Lines no header owns — reasoning, or the tail of output from before
|
|
20
31
|
* the recorded events begin — are prose. Everything here becomes TEXT on the
|
package/lib/api/harness.js
CHANGED
|
@@ -129,6 +129,13 @@ export function sections(diff) {
|
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
131
|
const HEAD = /^(exec|codex|thinking|user|apply patch|tokens used|turn interrupted|context compacted)$/;
|
|
132
|
+
/** The older harness's stamp on each item it starts. */
|
|
133
|
+
const STAMP = /^\[\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z?\] /;
|
|
134
|
+
/** How long a stamped command or tool took: `14ms`, `2.1s`, `1m 05s`, `1h 02m`. */
|
|
135
|
+
const TOOK = '(?: in [\\dhms. ]+)?';
|
|
136
|
+
const RAN = new RegExp(`^(.+) (succeeded|exited (-?\\d+))${TOOK}:$`);
|
|
137
|
+
const CALLED = new RegExp(`^(.+) (success|failed)${TOOK}:$`);
|
|
138
|
+
const PATCHED = new RegExp(`^apply_patch\\(.*\\) exited (-?\\d+)${TOOK}:$`);
|
|
132
139
|
const LEAD = /^(patch|mcp|web search|warning|ERROR|deprecated|model rerouted|hook): /;
|
|
133
140
|
const RESULT = /^\s*(succeeded|exited (-?\d+)|declined|in progress)( in [\d.]+m?s)?:$/;
|
|
134
141
|
/** A reader that appends the cards it finds to `out`, keyed by `mint`. */
|
|
@@ -168,8 +175,181 @@ export function harness(out, mint) {
|
|
|
168
175
|
out.push(c);
|
|
169
176
|
return c;
|
|
170
177
|
};
|
|
178
|
+
// The agent's log: its warnings, errors and diagnostics, one step for the run.
|
|
179
|
+
let log = null;
|
|
180
|
+
const logged = (text) => {
|
|
181
|
+
if (!log)
|
|
182
|
+
log = step('Agent log', '', 'done');
|
|
183
|
+
log.output += log.output ? `\n${text}` : text;
|
|
184
|
+
};
|
|
185
|
+
let stamped = false;
|
|
186
|
+
let sink = 'none';
|
|
187
|
+
let into = null;
|
|
188
|
+
// A stamped command or tool is ended by name: its text as the harness printed it.
|
|
189
|
+
const running = new Map();
|
|
190
|
+
const begin = (item, name) => {
|
|
191
|
+
running.set(name, item);
|
|
192
|
+
};
|
|
193
|
+
const end = (name, ok) => {
|
|
194
|
+
const item = running.get(name);
|
|
195
|
+
if (!item)
|
|
196
|
+
return null;
|
|
197
|
+
running.delete(name);
|
|
198
|
+
item.ran = ok ? 'done' : 'error';
|
|
199
|
+
return item;
|
|
200
|
+
};
|
|
201
|
+
const stamp = (l) => {
|
|
202
|
+
settle();
|
|
203
|
+
into = null;
|
|
204
|
+
sink = 'none';
|
|
205
|
+
if (/^Hanzo Dev v\S+$/.test(l)) {
|
|
206
|
+
rules = 0;
|
|
207
|
+
sink = 'banner';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (l.startsWith('binary: ')) {
|
|
211
|
+
sink = 'banner';
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (l === 'User instructions:') {
|
|
215
|
+
const c = { kind: 'said', key: mint(), who: 'person', text: '' };
|
|
216
|
+
out.push(c);
|
|
217
|
+
tasks.push({ card: c, lines: [] });
|
|
218
|
+
sink = 'task';
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (l === 'dev') {
|
|
222
|
+
into = said('');
|
|
223
|
+
sink = 'said';
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (l === 'thinking') {
|
|
227
|
+
into = step('Thinking', '', 'done');
|
|
228
|
+
sink = 'out';
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const exec = /^exec (.+) in (\S+)$/.exec(l);
|
|
232
|
+
if (exec) {
|
|
233
|
+
const file = reads(exec[1]);
|
|
234
|
+
const c = file
|
|
235
|
+
? { kind: 'read', key: mint(), file: shown(file), ran: 'running' }
|
|
236
|
+
: { kind: 'shell', key: mint(), command: unwrap(exec[1]), output: '', ran: 'running' };
|
|
237
|
+
out.push(c);
|
|
238
|
+
begin(c, exec[1]);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const ran = RAN.exec(l);
|
|
242
|
+
if (ran) {
|
|
243
|
+
const c = end(ran[1], ran[2] === 'succeeded');
|
|
244
|
+
if (c) {
|
|
245
|
+
// A read's output is the file it read, which the chip already names.
|
|
246
|
+
into = c;
|
|
247
|
+
sink = c.kind === 'shell' ? 'out' : 'drop';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const custom = /^tool (.+) (success|failed)$/.exec(l);
|
|
252
|
+
if (custom) {
|
|
253
|
+
into = end(custom[1], custom[2] === 'success') ?? step(custom[1], 'tool', custom[2] === 'success' ? 'done' : 'error');
|
|
254
|
+
sink = 'out';
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const tool = /^tool (.+)$/.exec(l);
|
|
258
|
+
if (tool) {
|
|
259
|
+
into = step(tool[1], 'tool', 'running');
|
|
260
|
+
begin(into, tool[1]);
|
|
261
|
+
sink = 'out';
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const called = CALLED.exec(l);
|
|
265
|
+
if (called && running.has(called[1])) {
|
|
266
|
+
into = end(called[1], called[2] === 'success');
|
|
267
|
+
sink = 'out';
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const search = /^🌐 Search: (.*)$/.exec(l);
|
|
271
|
+
if (search) {
|
|
272
|
+
step('Web search', search[1], 'done');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (/^apply_patch auto_approved=\w+:$/.test(l)) {
|
|
276
|
+
patch();
|
|
277
|
+
sink = 'patch';
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const patched = PATCHED.exec(l);
|
|
281
|
+
if (patched) {
|
|
282
|
+
if (edit)
|
|
283
|
+
edit.card.ran = patched[1] === '0' ? 'done' : 'error';
|
|
284
|
+
sink = 'drop';
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (l === 'turn diff:') {
|
|
288
|
+
diff = [];
|
|
289
|
+
sink = 'diff';
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (/^tokens used: [\d,]+$/.test(l))
|
|
293
|
+
return;
|
|
294
|
+
if (/^task (interrupted|aborted)/.test(l)) {
|
|
295
|
+
for (const item of running.values())
|
|
296
|
+
item.ran = 'cancelled';
|
|
297
|
+
running.clear();
|
|
298
|
+
out.push({ kind: 'note', key: mint(), text: 'The agent was interrupted' });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
logged(l);
|
|
302
|
+
sink = 'log';
|
|
303
|
+
};
|
|
304
|
+
// An unstamped line of the stamped grammar, where the last stamped line said.
|
|
305
|
+
const under = (l) => {
|
|
306
|
+
switch (sink) {
|
|
307
|
+
case 'banner':
|
|
308
|
+
if (l === '--------' && ++rules === 3)
|
|
309
|
+
sink = 'none';
|
|
310
|
+
return;
|
|
311
|
+
case 'task':
|
|
312
|
+
tasks[tasks.length - 1].lines.push(l);
|
|
313
|
+
return;
|
|
314
|
+
case 'said':
|
|
315
|
+
if (into && into.kind === 'said')
|
|
316
|
+
into.text += into.text ? `\n${l}` : l;
|
|
317
|
+
return;
|
|
318
|
+
case 'out':
|
|
319
|
+
if (into && (into.kind === 'shell' || into.kind === 'step'))
|
|
320
|
+
into.output += into.output ? `\n${l}` : l;
|
|
321
|
+
return;
|
|
322
|
+
case 'patch': {
|
|
323
|
+
const file = /^[AMD] (.+?)(?: -> (.+))?$/.exec(l);
|
|
324
|
+
if (file && edit)
|
|
325
|
+
edit.paths.push(file[2] ?? file[1]);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
case 'diff':
|
|
329
|
+
diff.push(l);
|
|
330
|
+
return;
|
|
331
|
+
case 'log':
|
|
332
|
+
if (l.trim())
|
|
333
|
+
logged(l);
|
|
334
|
+
return;
|
|
335
|
+
case 'drop':
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (l.trim())
|
|
339
|
+
logged(l);
|
|
340
|
+
};
|
|
171
341
|
const line = (raw) => {
|
|
172
342
|
const l = raw.replace(ANSI, '').replace(/\r$/, '');
|
|
343
|
+
const at = STAMP.exec(l);
|
|
344
|
+
if (at) {
|
|
345
|
+
stamped = true;
|
|
346
|
+
stamp(l.slice(at[0].length));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (stamped) {
|
|
350
|
+
under(l);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
173
353
|
if (mode === 'config') {
|
|
174
354
|
if (l === '--------' && ++rules === 2)
|
|
175
355
|
mode = 'prose';
|
|
@@ -286,10 +466,10 @@ export function harness(out, mint) {
|
|
|
286
466
|
mode = 'prose';
|
|
287
467
|
return;
|
|
288
468
|
}
|
|
289
|
-
const warn = /^(warning|ERROR|deprecated|model rerouted): ?(.*)$/.exec(l);
|
|
469
|
+
const warn = mode === 'output' || mode === 'agent' ? null : /^(warning|ERROR|deprecated|model rerouted): ?(.*)$/.exec(l);
|
|
290
470
|
if (warn) {
|
|
291
471
|
settle();
|
|
292
|
-
|
|
472
|
+
logged(l);
|
|
293
473
|
card = null;
|
|
294
474
|
mode = 'prose';
|
|
295
475
|
return;
|
package/lib/api/turn.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Event } from './sessions.ts';
|
|
2
2
|
/** The payload as an object. A live frame may carry it as a JSON string. */
|
|
3
3
|
export declare function decode(payload: unknown): Record<string, unknown> | string | null;
|
|
4
|
+
/**
|
|
5
|
+
* A run's error as one plain line with what to do, never the log it came from:
|
|
6
|
+
* that goes in the Error step beside it (cards).
|
|
7
|
+
*/
|
|
8
|
+
export declare function plain(error: string): string;
|
|
4
9
|
/**
|
|
5
10
|
* One event's sentence, or '' when it says nothing a person needs. `mode` is
|
|
6
11
|
* the run's, from its record: a plan run's final status says the plan.
|
package/lib/api/turn.js
CHANGED
|
@@ -70,7 +70,7 @@ function status(b, mode) {
|
|
|
70
70
|
return branch ? `Pushed ${branch}${tail}${site}` : `Done${tail}${site}`;
|
|
71
71
|
}
|
|
72
72
|
case 'error':
|
|
73
|
-
return str(b.error)
|
|
73
|
+
return plain(str(b.error));
|
|
74
74
|
case 'stopped':
|
|
75
75
|
return branch && b.changed ? `Stopped — work kept on ${branch}` : 'Stopped';
|
|
76
76
|
case 'paused':
|
|
@@ -81,6 +81,20 @@ function status(b, mode) {
|
|
|
81
81
|
}
|
|
82
82
|
return str(b.status);
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* A run's error as one plain line with what to do, never the log it came from:
|
|
86
|
+
* that goes in the Error step beside it (cards).
|
|
87
|
+
*/
|
|
88
|
+
export function plain(error) {
|
|
89
|
+
if (!error)
|
|
90
|
+
return 'The run hit an error';
|
|
91
|
+
const ours = /providers_exhausted|every provider refused|temporarily unavailable/i.test(error);
|
|
92
|
+
if (!ours && /\b402 Payment Required|insufficient (credit|balance)/i.test(error))
|
|
93
|
+
return 'Your balance is out of credit. Add credit, then try again.';
|
|
94
|
+
if (ours || /\b(429 Too Many Requests|503 Service Unavailable)|stream disconnected|retries exhausted|rate limit/i.test(error))
|
|
95
|
+
return 'The model did not answer. Try again in a moment, or pick another model.';
|
|
96
|
+
return 'The run stopped with an error. Try again, or follow up with what to change.';
|
|
97
|
+
}
|
|
84
98
|
/**
|
|
85
99
|
* One event's sentence, or '' when it says nothing a person needs. `mode` is
|
|
86
100
|
* the run's, from its record: a plan run's final status says the plan.
|
|
@@ -353,6 +367,8 @@ export function cards(events, mode = '') {
|
|
|
353
367
|
const line = status(b, mode);
|
|
354
368
|
if (line)
|
|
355
369
|
out.push({ kind: 'note', key: mint(), text: line });
|
|
370
|
+
if (str(b.status) === 'error' && str(b.error))
|
|
371
|
+
out.push({ kind: 'step', key: mint(), name: 'Error', detail: '', output: str(b.error), ran: 'error' });
|
|
356
372
|
break;
|
|
357
373
|
}
|
|
358
374
|
case 'control': {
|
package/lib/desk.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DropdownMenuProps } from '@hanzo/ui';
|
|
2
2
|
import type { Event } from './api/sessions.ts';
|
|
3
|
-
export declare function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }: {
|
|
3
|
+
export declare function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, site, published, events, live, menu, refused, retry, onRetry, onHide, }: {
|
|
4
4
|
id: string;
|
|
5
5
|
repo: string;
|
|
6
6
|
branch: string;
|
|
@@ -15,6 +15,10 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ti
|
|
|
15
15
|
project: string;
|
|
16
16
|
/** The sandbox the run holds, or '' before it leases one and on a machine. */
|
|
17
17
|
sandbox: string;
|
|
18
|
+
/** Where the run's work is published, or '' before it is. */
|
|
19
|
+
site: string;
|
|
20
|
+
/** The run's latest publish, which loads the page again. */
|
|
21
|
+
published: number;
|
|
18
22
|
events: Event[];
|
|
19
23
|
live: boolean;
|
|
20
24
|
/** What can be done to the run — rename, share, copy its id — as its header offers it. */
|
package/lib/desk.js
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The pane beside a run's transcript:
|
|
4
|
-
*
|
|
3
|
+
* The pane beside a run's transcript, as tabs: the Browser showing where the run
|
|
4
|
+
* is published, its sandbox's Desktop and Terminal, what it produced, its files,
|
|
5
|
+
* what it pushed, and its codebase's environment.
|
|
5
6
|
*/
|
|
6
7
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
7
|
-
import { Info, MoreHorizontal, PanelRightClose } from '@hanzogui/lucide-icons-2';
|
|
8
|
+
import { ExternalLink, Info, MoreHorizontal, PanelRightClose, RefreshCcw } from '@hanzogui/lucide-icons-2';
|
|
8
9
|
import { Button, DropdownMenu } from '@hanzo/ui';
|
|
9
|
-
import {
|
|
10
|
+
import { PreviewFrame } from '@hanzo/ui/agents';
|
|
11
|
+
import { useRef, useState } from 'react';
|
|
10
12
|
import { shell } from './api/turn.js';
|
|
11
13
|
import { Door } from './door.js';
|
|
12
14
|
import { Environment } from './environment.js';
|
|
13
|
-
import { Files } from './files.js';
|
|
15
|
+
import { Artifacts, Files } from './files.js';
|
|
14
16
|
import { Git } from './git.js';
|
|
17
|
+
import { Out } from './out.js';
|
|
15
18
|
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
16
|
-
const BLEED = ['desktop', 'terminal'];
|
|
17
|
-
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }) {
|
|
18
|
-
// Until a tab is picked it follows the run
|
|
19
|
-
//
|
|
19
|
+
const BLEED = ['browser', 'desktop', 'terminal'];
|
|
20
|
+
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, site, published, events, live, menu, refused, retry, onRetry, onHide, }) {
|
|
21
|
+
// Until a tab is picked it follows the run: a setup run's work IS the
|
|
22
|
+
// environment, a published run opens on its site, any other on its terminal.
|
|
20
23
|
const [picked, setTab] = useState(null);
|
|
21
|
-
const tab = picked ?? (mode === 'setup' ? 'environment' : 'terminal');
|
|
24
|
+
const tab = picked ?? (mode === 'setup' ? 'environment' : site ? 'browser' : 'terminal');
|
|
22
25
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
23
26
|
const lines = shell(events);
|
|
24
27
|
const bleed = BLEED.includes(tab);
|
|
25
|
-
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: "
|
|
28
|
+
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') }] })] }), _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: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { 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, live: live, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { 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: live }) : null] })] }));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The Browser tab: the page where the run published its work, framed, with its
|
|
32
|
+
* address, a reload and the page in a tab of its own.
|
|
33
|
+
*/
|
|
34
|
+
function Browser({ site, live }) {
|
|
35
|
+
const frame = useRef(null);
|
|
36
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Reload the page", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: site || 'Not published yet' }), site ? (_jsx(Out, { href: site, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: site, title: "Where this run is published", device: "desktop", empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: live ? 'Not published yet' : 'Nothing was published' }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: live ? 'The page appears here once the run publishes its site.' : 'This run published no site. Its work is on its branch.' })] }) })] }));
|
|
26
37
|
}
|
|
27
38
|
function TabButton({ id, tab, onPick, children }) {
|
|
28
39
|
const on = tab === id;
|
package/lib/files.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
export declare function Files({ session, repo, branch, sandbox, live,
|
|
1
|
+
export declare function Files({ session, repo, branch, sandbox, live, }: {
|
|
2
2
|
session: string;
|
|
3
3
|
repo: string;
|
|
4
4
|
branch: string;
|
|
5
5
|
sandbox: string;
|
|
6
6
|
live: boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function Artifacts({ repo, branch, mode, project, site, pr, onEnvironment, }: {
|
|
9
|
+
repo: string;
|
|
10
|
+
branch: string;
|
|
7
11
|
mode: string;
|
|
8
12
|
project: string;
|
|
13
|
+
/** Where the run's work is published, or ''. */
|
|
14
|
+
site: string;
|
|
9
15
|
pr: {
|
|
10
16
|
href: string;
|
|
11
17
|
label: string;
|
package/lib/files.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* A run's Files tab
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* request, its branch, its environment proposal and
|
|
3
|
+
* A run's Files tab: its working tree, live from the sandbox while the run holds
|
|
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.
|
|
7
7
|
*/
|
|
8
8
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
-
import { ExternalLink, File, Folder, GitBranch, GitPullRequest, Layers, Settings2 } from '@hanzogui/lucide-icons-2';
|
|
9
|
+
import { ExternalLink, File, Folder, Globe, GitBranch, GitPullRequest, Layers, Settings2 } from '@hanzogui/lucide-icons-2';
|
|
10
10
|
import { useState } from 'react';
|
|
11
11
|
import { blob, tree } from './api/changes.js';
|
|
12
12
|
import { read as look } from './api/sandbox.js';
|
|
@@ -14,12 +14,11 @@ import { useRead } from './data.js';
|
|
|
14
14
|
import { useHost, useTarget } from './host.js';
|
|
15
15
|
import { Out } from './out.js';
|
|
16
16
|
const monoWrap = { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
17
|
-
export function Files({ session, repo, branch, sandbox, live,
|
|
18
|
-
const [pane, setPane] = useState('files');
|
|
17
|
+
export function Files({ session, repo, branch, sandbox, live, }) {
|
|
19
18
|
const reachable = live && Boolean(sandbox);
|
|
20
19
|
const [source, setSource] = useState(reachable ? 'live' : 'branch');
|
|
21
20
|
const from = reachable ? source : 'branch';
|
|
22
|
-
return (_jsxs(YStack, { gap: "$3", children: [_jsxs(XStack, { gap: "$1", items: "center", flexWrap: "wrap", children: [_jsx(Pick, { on:
|
|
21
|
+
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}` : ''}` })] }));
|
|
23
22
|
}
|
|
24
23
|
/** The sandbox's working tree, as it is right now. */
|
|
25
24
|
function Live({ sandbox }) {
|
|
@@ -85,9 +84,11 @@ function Branch({ session, label }) {
|
|
|
85
84
|
function Listing({ where, up, error, loading, rows, file, body, }) {
|
|
86
85
|
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] }));
|
|
87
86
|
}
|
|
88
|
-
function Artifacts({ repo, branch, mode, project, pr, onEnvironment, }) {
|
|
87
|
+
export function Artifacts({ repo, branch, mode, project, site, pr, onEnvironment, }) {
|
|
89
88
|
const host = useHost();
|
|
90
89
|
const items = [];
|
|
90
|
+
if (site)
|
|
91
|
+
items.push({ key: 'site', icon: _jsx(Globe, { size: 16 }), title: site.replace(/^https:\/\//, ''), note: 'Where this run is published', href: site });
|
|
91
92
|
if (pr.href)
|
|
92
93
|
items.push({ key: 'pr', icon: _jsx(GitPullRequest, { size: 16 }), title: `Pull request ${pr.label}`, note: 'Opened on the forge', href: pr.href });
|
|
93
94
|
if (branch)
|
package/lib/run.js
CHANGED
|
@@ -35,7 +35,7 @@ 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';
|
|
37
37
|
import { verdict } from './api/verdict.js';
|
|
38
|
-
import { useKept, useRead, useRun } from './data.js';
|
|
38
|
+
import { useKept, useProjects, useRead, useRun } from './data.js';
|
|
39
39
|
import { Desk } from './desk.js';
|
|
40
40
|
import { useHost, useTarget } from './host.js';
|
|
41
41
|
import { EFFORTS } from './landing.js';
|
|
@@ -74,6 +74,15 @@ export function Run({ id }) {
|
|
|
74
74
|
const plan = useMemo(() => steps(events), [events]);
|
|
75
75
|
const kept = useMemo(() => settled(events), [events]);
|
|
76
76
|
const state = status || end.status;
|
|
77
|
+
// Where the run's work is published: its project's own address, read again when
|
|
78
|
+
// the run says it went live, never an address an event names.
|
|
79
|
+
const built = useProjects(t, signed && Boolean(record?.project));
|
|
80
|
+
const rebuilt = built.reload;
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (end.published)
|
|
83
|
+
rebuilt();
|
|
84
|
+
}, [end.published, rebuilt]);
|
|
85
|
+
const site = built.value.find((p) => p.slug === record?.project)?.live ?? '';
|
|
77
86
|
const pr = pull(record?.pr ?? '', record?.repo ?? '');
|
|
78
87
|
const live = LIVE.has(state);
|
|
79
88
|
const running = signed && live && !detail.error;
|
|
@@ -290,7 +299,7 @@ export function Run({ id }) {
|
|
|
290
299
|
: 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
|
|
291
300
|
...(paused ? ['pause'] : ['resume']),
|
|
292
301
|
...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
|
|
293
|
-
] }), _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 ?? '', 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) => {
|
|
302
|
+
] }), _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) => {
|
|
294
303
|
if (e.key === 'Enter')
|
|
295
304
|
save();
|
|
296
305
|
} }), _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.
|
|
3
|
+
"version": "0.2.29",
|
|
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.",
|