@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.
@@ -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
- /** The earlier run's ask, without the codebase its title leads with (`universe: add the widget`). */
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
- /** The earlier run's ask, without the codebase its title leads with (`universe: add the widget`). */
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
- return name && t.startsWith(`${name}: `) ? t.slice(name.length + 2).trim() : t;
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
+ }
@@ -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
@@ -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
- out.push({ kind: 'note', key: mint(), text: warn[1] === 'ERROR' ? warn[2] : `${warn[1]}: ${warn[2]}` });
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) || 'The run hit an 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: its codebase's environment, what it
4
- * pushed, its sandbox's desktop and shell, its files, and what it listens to.
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 { useState } from 'react';
10
- import { shell } from './api/turn.js';
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's mode, which is known once the run
19
- // is read: a setup run's work IS the environment, so it opens there.
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: "environment", tab: tab, onPick: setTab, children: "Environment" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "subscriptions", tab: tab, onPick: setTab, children: "Subscriptions" })] }), _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 === '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 === 'files' ? (_jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: live, mode: mode, project: project, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'subscriptions' ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", px: "$6", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 320 }, children: "Ask an agent to subscribe to a Slack channel or thread, a pull request, or a timer." }) })) : null] })] }));
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, mode, project, pr, onEnvironment, }: {
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 { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * A run's Files tab. Files is its working tree: live from the sandbox while the
4
- * run holds one — every edit the agent has made, committed or not — and its
5
- * branch on the forge otherwise. Artifacts is what the run produced: its pull
6
- * request, its branch, its environment proposal and the project it built.
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, mode, project, pr, onEnvironment, }) {
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: pane === 'files', onPress: () => setPane('files'), children: "Files" }), _jsx(Pick, { on: pane === 'artifacts', onPress: () => setPane('artifacts'), children: "Artifacts" }), _jsx(XStack, { flex: 1 }), pane === 'files' && reachable ? (_jsxs(_Fragment, { children: [_jsx(Pick, { on: from === 'live', onPress: () => setSource('live'), children: "Live" }), _jsx(Pick, { on: from === 'branch', onPress: () => setSource('branch'), children: "Branch" })] })) : null] }), pane === 'artifacts' ? (_jsx(Artifacts, { repo: repo, branch: branch, mode: mode, project: project, pr: pr, onEnvironment: onEnvironment })) : from === 'live' ? (_jsx(Live, { sandbox: sandbox })) : (_jsx(Branch, { session: session, label: `${repo}${branch ? `@${branch}` : ''}` }))] }));
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 run = await start(t, tuned(how === 'approve' ? approve(earlier, said) : followUp(earlier, said)));
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.28",
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.",