@hanzo/build 0.2.28 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
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,20 @@ function status(b, mode) {
81
81
  }
82
82
  return str(b.status);
83
83
  }
84
+ /**
85
+ * A run's error as one plain line with what to do, never the log it came from:
86
+ * that goes in the Error step beside it (cards).
87
+ */
88
+ export function plain(error) {
89
+ if (!error)
90
+ return 'The run hit an error';
91
+ const ours = /providers_exhausted|every provider refused|temporarily unavailable/i.test(error);
92
+ if (!ours && /\b402 Payment Required|insufficient (credit|balance)/i.test(error))
93
+ return 'Your balance is out of credit. Add credit, then try again.';
94
+ if (ours || /\b(429 Too Many Requests|503 Service Unavailable)|stream disconnected|retries exhausted|rate limit/i.test(error))
95
+ return 'The model did not answer. Try again in a moment, or pick another model.';
96
+ return 'The run stopped with an error. Try again, or follow up with what to change.';
97
+ }
84
98
  /**
85
99
  * One event's sentence, or '' when it says nothing a person needs. `mode` is
86
100
  * the run's, from its record: a plan run's final status says the plan.
@@ -353,6 +367,8 @@ export function cards(events, mode = '') {
353
367
  const line = status(b, mode);
354
368
  if (line)
355
369
  out.push({ kind: 'note', key: mint(), text: line });
370
+ if (str(b.status) === 'error' && str(b.error))
371
+ out.push({ kind: 'step', key: mint(), name: 'Error', detail: '', output: str(b.error), ran: 'error' });
356
372
  break;
357
373
  }
358
374
  case 'control': {
package/lib/desk.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type DropdownMenuProps } from '@hanzo/ui';
2
2
  import type { Event } from './api/sessions.ts';
3
- export declare function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }: {
3
+ export declare function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, site, published, events, live, menu, refused, retry, onRetry, onHide, }: {
4
4
  id: string;
5
5
  repo: string;
6
6
  branch: string;
@@ -15,6 +15,10 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ti
15
15
  project: string;
16
16
  /** The sandbox the run holds, or '' before it leases one and on a machine. */
17
17
  sandbox: string;
18
+ /** Where the run's work is published, or '' before it is. */
19
+ site: string;
20
+ /** The run's latest publish, which loads the page again. */
21
+ published: number;
18
22
  events: Event[];
19
23
  live: boolean;
20
24
  /** What can be done to the run — rename, share, copy its id — as its header offers it. */
package/lib/desk.js CHANGED
@@ -1,28 +1,39 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * The pane beside a run's transcript: 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 { PreviewFrame } from '@hanzo/ui/agents';
11
+ import { useRef, useState } from 'react';
10
12
  import { shell } from './api/turn.js';
11
13
  import { Door } from './door.js';
12
14
  import { Environment } from './environment.js';
13
- import { Files } from './files.js';
15
+ import { Artifacts, Files } from './files.js';
14
16
  import { Git } from './git.js';
17
+ import { Out } from './out.js';
15
18
  /** The tabs that are a surface of their own, drawn edge to edge. */
16
- const BLEED = ['desktop', 'terminal'];
17
- export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }) {
18
- // Until a tab is picked it follows the run'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);
24
27
  const bleed = BLEED.includes(tab);
25
- return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "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] })] }));
28
+ return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "browser", tab: tab, onPick: setTab, children: "Browser" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "artifacts", tab: tab, onPick: setTab, children: "Artifacts" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" })] }), _jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { site: site, live: live }, published) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, live: live, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { repo: repo, branch: branch, mode: mode, project: project, site: site, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'files' ? _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: live }) : null] })] }));
29
+ }
30
+ /**
31
+ * The Browser tab: the page where the run published its work, framed, with its
32
+ * address, a reload and the page in a tab of its own.
33
+ */
34
+ function Browser({ site, live }) {
35
+ const frame = useRef(null);
36
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Reload the page", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: site || 'Not published yet' }), site ? (_jsx(Out, { href: site, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: site, title: "Where this run is published", device: "desktop", empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: live ? 'Not published yet' : 'Nothing was published' }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: live ? 'The page appears here once the run publishes its site.' : 'This run published no site. Its work is on its branch.' })] }) })] }));
26
37
  }
27
38
  function TabButton({ id, tab, onPick, children }) {
28
39
  const on = tab === id;
package/lib/files.d.ts CHANGED
@@ -1,11 +1,17 @@
1
- export declare function Files({ session, repo, branch, sandbox, live, 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
@@ -35,7 +35,7 @@ import { ENSO, label as named, models } from './api/models.js';
35
35
  import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
36
36
  import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
37
37
  import { verdict } from './api/verdict.js';
38
- import { useKept, useRead, useRun } from './data.js';
38
+ import { useKept, useProjects, useRead, useRun } from './data.js';
39
39
  import { Desk } from './desk.js';
40
40
  import { useHost, useTarget } from './host.js';
41
41
  import { EFFORTS } from './landing.js';
@@ -74,6 +74,15 @@ export function Run({ id }) {
74
74
  const plan = useMemo(() => steps(events), [events]);
75
75
  const kept = useMemo(() => settled(events), [events]);
76
76
  const state = status || end.status;
77
+ // Where the run's work is published: its project's own address, read again when
78
+ // the run says it went live, never an address an event names.
79
+ const built = useProjects(t, signed && Boolean(record?.project));
80
+ const rebuilt = built.reload;
81
+ useEffect(() => {
82
+ if (end.published)
83
+ rebuilt();
84
+ }, [end.published, rebuilt]);
85
+ const site = built.value.find((p) => p.slug === record?.project)?.live ?? '';
77
86
  const pr = pull(record?.pr ?? '', record?.repo ?? '');
78
87
  const live = LIVE.has(state);
79
88
  const running = signed && live && !detail.error;
@@ -290,7 +299,7 @@ export function Run({ id }) {
290
299
  : 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
291
300
  ...(paused ? ['pause'] : ['resume']),
292
301
  ...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
293
- ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
302
+ ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', site: site, published: end.published, events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
294
303
  if (e.key === 'Enter')
295
304
  save();
296
305
  } }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",