@hanzo/build 0.2.28 → 0.2.30
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 +11 -1
- package/lib/api/coding.js +17 -2
- package/lib/api/harness.d.ts +11 -0
- package/lib/api/harness.js +182 -2
- package/lib/api/turn.d.ts +7 -0
- package/lib/api/turn.js +32 -1
- package/lib/desk.d.ts +5 -1
- package/lib/desk.js +28 -16
- package/lib/files.d.ts +7 -1
- package/lib/files.js +11 -10
- package/lib/run.js +19 -5
- package/lib/transcript.js +1 -1
- package/package.json +1 -1
package/lib/api/coding.d.ts
CHANGED
|
@@ -78,7 +78,11 @@ export interface Earlier {
|
|
|
78
78
|
/** Its work is on its own branch (turn.ts `settled`). */
|
|
79
79
|
pushed: boolean;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The earlier run's ask, without the codebase its title leads with (`universe:
|
|
83
|
+
* add the widget`), or the `new project:` the platform titles a run that made its
|
|
84
|
+
* codebase with (apps/coding start.go).
|
|
85
|
+
*/
|
|
82
86
|
export declare function headline(title: string, repo: string): string;
|
|
83
87
|
/**
|
|
84
88
|
* A new run continuing an earlier one in its mode: what the person said, and
|
|
@@ -89,3 +93,9 @@ export declare function headline(title: string, repo: string): string;
|
|
|
89
93
|
export declare function followUp(e: Earlier, said: string): Ask;
|
|
90
94
|
/** A build of what a plan run answered: its ask as the title, and the plan to carry out. */
|
|
91
95
|
export declare function approve(e: Earlier, plan: string): Ask;
|
|
96
|
+
/**
|
|
97
|
+
* The same ask again, where the earlier run worked and in its mode: what a failed
|
|
98
|
+
* run's Try again starts. `ask` is the person's words as the run recorded them;
|
|
99
|
+
* with none recorded, its title's.
|
|
100
|
+
*/
|
|
101
|
+
export declare function retry(e: Earlier, ask: string): Ask;
|
package/lib/api/coding.js
CHANGED
|
@@ -63,11 +63,18 @@ export async function start(t, ask) {
|
|
|
63
63
|
target: s('targetId'),
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The earlier run's ask, without the codebase its title leads with (`universe:
|
|
68
|
+
* add the widget`), or the `new project:` the platform titles a run that made its
|
|
69
|
+
* codebase with (apps/coding start.go).
|
|
70
|
+
*/
|
|
67
71
|
export function headline(title, repo) {
|
|
68
72
|
const name = repo.split('/').filter(Boolean).pop() ?? '';
|
|
69
73
|
const t = title.trim();
|
|
70
|
-
|
|
74
|
+
for (const lead of [name, 'new project'])
|
|
75
|
+
if (lead && t.startsWith(`${lead}: `))
|
|
76
|
+
return t.slice(lead.length + 2).trim();
|
|
77
|
+
return t;
|
|
71
78
|
}
|
|
72
79
|
/**
|
|
73
80
|
* The same codebase, place and project as the earlier run, starting from its
|
|
@@ -112,3 +119,11 @@ export function approve(e, plan) {
|
|
|
112
119
|
const ask = `Carry out this plan:\n\n${plan.trim()}`;
|
|
113
120
|
return { ...from(e), prompt: head ? `${head}\n\n${ask}` : ask, mode: 'build' };
|
|
114
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The same ask again, where the earlier run worked and in its mode: what a failed
|
|
124
|
+
* run's Try again starts. `ask` is the person's words as the run recorded them;
|
|
125
|
+
* with none recorded, its title's.
|
|
126
|
+
*/
|
|
127
|
+
export function retry(e, ask) {
|
|
128
|
+
return { ...from(e), prompt: ask.trim() || headline(e.title, e.repo), mode: modeOf(e.mode) };
|
|
129
|
+
}
|
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.
|
|
@@ -51,6 +56,8 @@ export interface Outcome {
|
|
|
51
56
|
* the branch and the pull request are read from the run's record (`pull`).
|
|
52
57
|
*/
|
|
53
58
|
export declare function outcome(events: Pick<Event, 'kind' | 'payload' | 'seq'>[]): Outcome;
|
|
59
|
+
/** Why the run failed, as its last status says it, or '' when that status is not a failure. */
|
|
60
|
+
export declare function failure(events: Pick<Event, 'kind' | 'payload' | 'seq'>[]): string;
|
|
54
61
|
/** The turns, ordered and deduped: a recorded read and the live feed overlap. */
|
|
55
62
|
export declare function merge<T extends Pick<Event, 'id' | 'seq'>>(...lists: T[][]): T[];
|
|
56
63
|
/** The actor, short enough to label a block: `hanzo/2d4d67ab`, not the whole subject. */
|
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,22 @@ 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 (/free sandboxes .*are used/i.test(error))
|
|
93
|
+
return 'This plan’s free sandboxes are used for now. Add credit to keep building, or try again later.';
|
|
94
|
+
if (!ours && /\b402 Payment Required|insufficient (credit|balance)|top up/i.test(error))
|
|
95
|
+
return 'Your balance is out of credit. Add credit, then try again.';
|
|
96
|
+
if (ours || /\b(429 Too Many Requests|503 Service Unavailable)|stream disconnected|retries exhausted|rate limit/i.test(error))
|
|
97
|
+
return 'The model did not answer. Try again in a moment, or pick another model.';
|
|
98
|
+
return 'The run stopped with an error. Try again, or follow up with what to change.';
|
|
99
|
+
}
|
|
84
100
|
/**
|
|
85
101
|
* One event's sentence, or '' when it says nothing a person needs. `mode` is
|
|
86
102
|
* the run's, from its record: a plan run's final status says the plan.
|
|
@@ -194,6 +210,19 @@ export function outcome(events) {
|
|
|
194
210
|
}
|
|
195
211
|
return out;
|
|
196
212
|
}
|
|
213
|
+
/** Why the run failed, as its last status says it, or '' when that status is not a failure. */
|
|
214
|
+
export function failure(events) {
|
|
215
|
+
let out = '';
|
|
216
|
+
for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
|
|
217
|
+
if (e.kind !== 'status')
|
|
218
|
+
continue;
|
|
219
|
+
const b = decode(e.payload);
|
|
220
|
+
if (!b || typeof b === 'string' || !str(b.status))
|
|
221
|
+
continue;
|
|
222
|
+
out = str(b.status) === 'error' ? str(b.error) || 'The run hit an error' : '';
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
197
226
|
/** The turns, ordered and deduped: a recorded read and the live feed overlap. */
|
|
198
227
|
export function merge(...lists) {
|
|
199
228
|
const seen = new Map();
|
|
@@ -353,6 +382,8 @@ export function cards(events, mode = '') {
|
|
|
353
382
|
const line = status(b, mode);
|
|
354
383
|
if (line)
|
|
355
384
|
out.push({ kind: 'note', key: mint(), text: line });
|
|
385
|
+
if (str(b.status) === 'error' && str(b.error))
|
|
386
|
+
out.push({ kind: 'step', key: mint(), name: 'Error', detail: '', output: str(b.error), ran: 'error' });
|
|
356
387
|
break;
|
|
357
388
|
}
|
|
358
389
|
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,40 @@
|
|
|
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 {
|
|
10
|
+
import { PreviewFrame } from '@hanzo/ui/agents';
|
|
11
|
+
import { useRef, useState } from 'react';
|
|
12
|
+
import { failure, 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);
|
|
27
|
+
const failed = failure(events);
|
|
24
28
|
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: "
|
|
29
|
+
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, failed: failed, 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] })] }));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The Browser tab: the page where the run published its work, framed, with its
|
|
33
|
+
* address, a reload and the page in a tab of its own.
|
|
34
|
+
*/
|
|
35
|
+
function Browser({ site, live }) {
|
|
36
|
+
const frame = useRef(null);
|
|
37
|
+
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
38
|
}
|
|
27
39
|
function TabButton({ id, tab, onPick, children }) {
|
|
28
40
|
const on = tab === id;
|
|
@@ -38,15 +50,15 @@ const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace:
|
|
|
38
50
|
* outlive the sandbox. The shell is a tmux session named for the run, so a
|
|
39
51
|
* reopened tab reattaches to it.
|
|
40
52
|
*/
|
|
41
|
-
function Terminal({ id, sandbox, lines, live, refused, retry, onRetry, }) {
|
|
53
|
+
function Terminal({ id, sandbox, lines, live, failed, refused, retry, onRetry, }) {
|
|
42
54
|
const reachable = live && Boolean(sandbox);
|
|
43
55
|
// The shell while there is one, unless the log was asked for.
|
|
44
56
|
const [log, setLog] = useState(false);
|
|
45
57
|
const showShell = reachable && !log;
|
|
46
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [reachable ? (_jsx(XStack, { render: "button", "aria-label": "Shell", onPress: () => setLog(false), px: "$2", py: "$1", rounded: "$2", bg: showShell ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: showShell ? '$ink' : '$soft', children: "Shell" }) })) : null, _jsx(XStack, { render: "button", "aria-label": "Agent log", onPress: () => setLog(true), px: "$2", py: "$1", rounded: "$2", bg: showShell ? 'transparent' : '$hover', children: _jsx(SizableText, { size: "$1", color: showShell ? '$soft' : '$ink', children: "Agent log" }) })] }), showShell ? _jsx(Door, { which: "terminal", sandbox: sandbox, live: live, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })] }));
|
|
58
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [reachable ? (_jsx(XStack, { render: "button", "aria-label": "Shell", onPress: () => setLog(false), px: "$2", py: "$1", rounded: "$2", bg: showShell ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: showShell ? '$ink' : '$soft', children: "Shell" }) })) : null, _jsx(XStack, { render: "button", "aria-label": "Agent log", onPress: () => setLog(true), px: "$2", py: "$1", rounded: "$2", bg: showShell ? 'transparent' : '$hover', children: _jsx(SizableText, { size: "$1", color: showShell ? '$soft' : '$ink', children: "Agent log" }) })] }), showShell ? _jsx(Door, { which: "terminal", sandbox: sandbox, live: live, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })] }));
|
|
47
59
|
}
|
|
48
|
-
function Log({ lines, live, refused, retry, onRetry, }) {
|
|
60
|
+
function Log({ lines, live, failed, refused, retry, onRetry, }) {
|
|
49
61
|
const prompt = 'workspace';
|
|
50
62
|
const idle = lines.length === 0 && !live;
|
|
51
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$3", gap: "$1", children: [idle ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", py: "$8", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: refused || 'This run finished without writing a command.' }), refused ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: retry })) : null] })) : (lines.map((line, i) => line.role === 'cmd' ? (_jsxs(XStack, { gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(SizableText, { flex: 1, size: "$1", color: "$ink", style: mono, children: line.text })] }, i)) : (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: line.text }, i)))), live ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(YStack, { width: 7, height: 14, bg: "$ink" })] })) : null] }), _jsx(XStack, { px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Commands appear here as the run writes them." }) })] }));
|
|
63
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$3", gap: "$1", children: [idle ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", py: "$8", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: refused || (failed ? `This run stopped before its first command: ${failed}` : 'This run finished without writing a command.') }), refused ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: retry })) : null] })) : (lines.map((line, i) => line.role === 'cmd' ? (_jsxs(XStack, { gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(SizableText, { flex: 1, size: "$1", color: "$ink", style: mono, children: line.text })] }, i)) : (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: line.text }, i)))), live ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(YStack, { width: 7, height: 14, bg: "$ink" })] })) : null] }), _jsx(XStack, { px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Commands appear here as the run writes them." }) })] }));
|
|
52
64
|
}
|
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
|
@@ -30,12 +30,12 @@ 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, 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';
|
|
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;
|
|
@@ -106,6 +115,10 @@ export function Run({ id }) {
|
|
|
106
115
|
}
|
|
107
116
|
: null;
|
|
108
117
|
const planned = mode === 'plan' && finished && state === 'done' ? answer(events) : '';
|
|
118
|
+
// A failed run is tried again with the person's words as it recorded them.
|
|
119
|
+
const failed = finished && state === 'error';
|
|
120
|
+
const asked = shown.find((c) => c.kind === 'said' && c.who === 'person');
|
|
121
|
+
const again = asked && asked.kind === 'said' ? asked.text : '';
|
|
109
122
|
// What the recorded read left out: it carries the latest fifty turns.
|
|
110
123
|
const hidden = Math.max(0, (detail.value?.events ?? 0) - (detail.value?.recent.length ?? 0));
|
|
111
124
|
// Only a run still working is on a step.
|
|
@@ -154,7 +167,8 @@ export function Run({ id }) {
|
|
|
154
167
|
const carry = async (said, how = 'follow') => {
|
|
155
168
|
if (!earlier)
|
|
156
169
|
throw new Error('This run is still being read');
|
|
157
|
-
const
|
|
170
|
+
const ask = how === 'approve' ? approve(earlier, said) : how === 'retry' ? retry(earlier, said) : followUp(earlier, said);
|
|
171
|
+
const run = await start(t, tuned(ask));
|
|
158
172
|
if (paused)
|
|
159
173
|
await stop(t, id, 'Continued in a follow-up run').catch(() => undefined);
|
|
160
174
|
setDraft('');
|
|
@@ -274,7 +288,7 @@ export function Run({ id }) {
|
|
|
274
288
|
? setup
|
|
275
289
|
? 'Setting up environment'
|
|
276
290
|
: 'Starting…'
|
|
277
|
-
: '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
|
|
291
|
+
: '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
|
|
278
292
|
? plan.map((s) => {
|
|
279
293
|
const on = s === current;
|
|
280
294
|
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));
|
|
@@ -290,7 +304,7 @@ export function Run({ id }) {
|
|
|
290
304
|
: 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
|
|
291
305
|
...(paused ? ['pause'] : ['resume']),
|
|
292
306
|
...(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) => {
|
|
307
|
+
] }), _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
308
|
if (e.key === 'Enter')
|
|
295
309
|
save();
|
|
296
310
|
} }), _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/lib/transcript.js
CHANGED
|
@@ -50,7 +50,7 @@ export function Transcript({ cards, live, empty, header, onApprove, approving =
|
|
|
50
50
|
case 'edit':
|
|
51
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));
|
|
52
52
|
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));
|
|
53
|
+
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
54
|
case 'note':
|
|
55
55
|
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: c.text }, c.key));
|
|
56
56
|
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.30",
|
|
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.",
|