@hanzo/build 0.2.29 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api/coding.d.ts +40 -4
- package/lib/api/coding.js +59 -12
- package/lib/api/sandbox.d.ts +41 -8
- package/lib/api/sandbox.js +98 -9
- package/lib/api/turn.d.ts +47 -14
- package/lib/api/turn.js +129 -89
- package/lib/desk.d.ts +1 -1
- package/lib/desk.js +191 -19
- package/lib/door.d.ts +7 -3
- package/lib/door.js +18 -7
- package/lib/files.d.ts +4 -1
- package/lib/files.js +61 -13
- package/lib/out.d.ts +6 -0
- package/lib/out.js +20 -0
- package/lib/project.js +12 -7
- package/lib/run.js +19 -5
- package/lib/transcript.js +12 -24
- package/package.json +1 -1
- package/lib/api/harness.d.ts +0 -62
- package/lib/api/harness.js +0 -556
package/lib/api/turn.js
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What a run's events SAY, as text a person reads.
|
|
3
|
-
*
|
|
4
|
-
* A curated projection, never a dump: an event carries the machinery a run
|
|
5
|
-
* happened on — hosts, paths, whole file bodies — and printing it would put
|
|
6
|
-
* someone's filesystem in front of anyone who can read the run. Each kind gets a
|
|
7
|
-
* sentence built from the few fields that describe the outcome; the rest is
|
|
8
|
-
* dropped by omission, because a projection cannot leak a field it never reads.
|
|
9
|
-
*
|
|
10
|
-
* Every string here is rendered as TEXT by the transcript. Nothing is HTML.
|
|
11
|
-
*
|
|
12
|
-
* The coding plane's vocabulary (apps/coding): `status` for a lifecycle move
|
|
13
|
-
* (started, routed, done, error, stopped, paused), `tool-call` for a step,
|
|
14
|
-
* `log` for a free line. A person's steering is a `control` {command, message}.
|
|
15
|
-
* A run on a project with a site ends `done` with `live` when it published what
|
|
16
|
-
* it pushed, or `unpublished` saying why it did not.
|
|
17
|
-
*
|
|
18
|
-
* `cards` is the transcript as it is drawn: the run's steps, and the agent's own
|
|
19
|
-
* output read by what each part is (harness.ts). The output a card shows is what
|
|
20
|
-
* the run's Terminal log already prints; a command's working directory, which is
|
|
21
|
-
* the sandbox's layout, is dropped.
|
|
22
|
-
*/
|
|
23
|
-
import { harness } from './harness.js';
|
|
24
1
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
2
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
25
3
|
/** The payload as an object. A live frame may carry it as a JSON string. */
|
|
26
4
|
export function decode(payload) {
|
|
27
5
|
if (payload && typeof payload === 'object')
|
|
@@ -89,7 +67,9 @@ export function plain(error) {
|
|
|
89
67
|
if (!error)
|
|
90
68
|
return 'The run hit an error';
|
|
91
69
|
const ours = /providers_exhausted|every provider refused|temporarily unavailable/i.test(error);
|
|
92
|
-
if (
|
|
70
|
+
if (/free sandboxes .*are used/i.test(error))
|
|
71
|
+
return 'This plan’s free sandboxes are used for now. Add credit to keep building, or try again later.';
|
|
72
|
+
if (!ours && /\b402 Payment Required|insufficient (credit|balance)|top up/i.test(error))
|
|
93
73
|
return 'Your balance is out of credit. Add credit, then try again.';
|
|
94
74
|
if (ours || /\b(429 Too Many Requests|503 Service Unavailable)|stream disconnected|retries exhausted|rate limit/i.test(error))
|
|
95
75
|
return 'The model did not answer. Try again in a moment, or pick another model.';
|
|
@@ -208,6 +188,19 @@ export function outcome(events) {
|
|
|
208
188
|
}
|
|
209
189
|
return out;
|
|
210
190
|
}
|
|
191
|
+
/** Why the run failed, as its last status says it, or '' when that status is not a failure. */
|
|
192
|
+
export function failure(events) {
|
|
193
|
+
let out = '';
|
|
194
|
+
for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
|
|
195
|
+
if (e.kind !== 'status')
|
|
196
|
+
continue;
|
|
197
|
+
const b = decode(e.payload);
|
|
198
|
+
if (!b || typeof b === 'string' || !str(b.status))
|
|
199
|
+
continue;
|
|
200
|
+
out = str(b.status) === 'error' ? str(b.error) || 'The run hit an error' : '';
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
211
204
|
/** The turns, ordered and deduped: a recorded read and the live feed overlap. */
|
|
212
205
|
export function merge(...lists) {
|
|
213
206
|
const seen = new Map();
|
|
@@ -238,31 +231,76 @@ export function pull(url, repo) {
|
|
|
238
231
|
return { href: '', label: '' };
|
|
239
232
|
return { href: url, label: `#${m[3]}` };
|
|
240
233
|
}
|
|
241
|
-
/** The platform's own words for the step that starts the agent (apps/coding sandboxrunner.go). */
|
|
242
|
-
const TASK = 'running the task';
|
|
243
234
|
/** What the run's own steps are called on a card. */
|
|
244
235
|
const NAMES = {
|
|
245
236
|
lease: 'Sandbox',
|
|
237
|
+
workspace: 'Workspace',
|
|
246
238
|
clone: 'Clone',
|
|
239
|
+
branch: 'Branch',
|
|
247
240
|
secrets: 'Secrets',
|
|
248
241
|
install: 'Install',
|
|
249
242
|
start: 'Start',
|
|
243
|
+
skills: 'Skills',
|
|
244
|
+
artifacts: 'Save artifacts',
|
|
250
245
|
push: 'Push',
|
|
251
|
-
|
|
246
|
+
kept: 'Keep the sandbox',
|
|
252
247
|
};
|
|
253
|
-
/** Steps another card already says: `leased` finishes the lease, `done` is the final status, `ended`
|
|
254
|
-
const QUIET = new Set(['leased', 'done', 'ended']);
|
|
248
|
+
/** Steps another card already says: `leased` finishes the lease, `done` is the final status, `ended` and `exit` are housekeeping. */
|
|
249
|
+
const QUIET = new Set(['leased', 'done', 'ended', 'exit']);
|
|
255
250
|
const ranOf = (status) => (status === 'ok' || status === 'done' ? 'done' : status === 'error' ? 'error' : 'running');
|
|
251
|
+
/** How an agent's item went: in progress, finished, or failed or declined. */
|
|
252
|
+
function itemRan(item) {
|
|
253
|
+
switch (str(item.status)) {
|
|
254
|
+
case 'in_progress':
|
|
255
|
+
return 'running';
|
|
256
|
+
case 'failed':
|
|
257
|
+
return 'error';
|
|
258
|
+
case 'declined':
|
|
259
|
+
return 'cancelled';
|
|
260
|
+
}
|
|
261
|
+
return typeof item.exit_code === 'number' && item.exit_code !== 0 ? 'error' : 'done';
|
|
262
|
+
}
|
|
263
|
+
/** A path in the workspace, without the sandbox's working directory above it. */
|
|
264
|
+
const rel = (path) => path.replace(/^\/work\//, '');
|
|
265
|
+
/**
|
|
266
|
+
* One item of the agent's narration as a card, or null when it says nothing a
|
|
267
|
+
* person reads. `key` is the card's, kept as the item is updated.
|
|
268
|
+
*/
|
|
269
|
+
function item(it, key) {
|
|
270
|
+
switch (str(it.type)) {
|
|
271
|
+
case 'agent_message':
|
|
272
|
+
return str(it.text) ? { kind: 'said', key, who: 'agent', text: str(it.text) } : null;
|
|
273
|
+
case 'reasoning':
|
|
274
|
+
return str(it.text) ? { kind: 'think', key, text: str(it.text) } : null;
|
|
275
|
+
case 'command_execution':
|
|
276
|
+
return { kind: 'shell', key, command: str(it.command), output: str(it.aggregated_output), ran: itemRan(it) };
|
|
277
|
+
case 'file_change': {
|
|
278
|
+
const changes = Array.isArray(it.changes) ? it.changes : [];
|
|
279
|
+
return { kind: 'edit', key, files: changes.map((c) => rel(str(obj(c).path))).filter(Boolean), ran: itemRan(it) };
|
|
280
|
+
}
|
|
281
|
+
case 'mcp_tool_call':
|
|
282
|
+
return { kind: 'step', key, name: str(it.tool) || 'Tool', detail: str(it.server), output: '', ran: itemRan(it) };
|
|
283
|
+
case 'web_search':
|
|
284
|
+
return { kind: 'step', key, name: 'Search', detail: str(it.query), output: '', ran: 'done' };
|
|
285
|
+
case 'todo_list': {
|
|
286
|
+
const items = Array.isArray(it.items) ? it.items : [];
|
|
287
|
+
return { kind: 'todo', key, items: items.map((x) => ({ text: str(obj(x).text), done: obj(x).completed === true })).filter((x) => x.text) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
256
292
|
/**
|
|
257
293
|
* A run's events, as the cards its transcript draws. `mode` is the run's, from
|
|
258
294
|
* its record: a plan's final status is its plan, and nothing else draws a plan.
|
|
259
295
|
*
|
|
260
|
-
* The run's own steps arrive as `tool-call` {step, message, status}
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* agent is the agent's
|
|
264
|
-
*
|
|
265
|
-
*
|
|
296
|
+
* The run's own steps arrive as `tool-call` {step, message, status}. The agent
|
|
297
|
+
* narrates itself as `event` lines: an item is started, updated and completed
|
|
298
|
+
* under one id, and its card is drawn once and kept current. Ids count from zero
|
|
299
|
+
* in each agent process, so an id is the agent's within the thread it started. A
|
|
300
|
+
* turn that fails is one plain line with the detail folded beneath it, drawn once
|
|
301
|
+
* however many times it is said; an `error` on its own is the agent retrying and
|
|
302
|
+
* is drawn only if the turn then fails. The final status carries the agent's
|
|
303
|
+
* answer, drawn once. `log` is never drawn here.
|
|
266
304
|
*/
|
|
267
305
|
export function cards(events, mode = '') {
|
|
268
306
|
const out = [];
|
|
@@ -270,48 +308,56 @@ export function cards(events, mode = '') {
|
|
|
270
308
|
let n = 0;
|
|
271
309
|
const mint = () => `${seq}.${n++}`;
|
|
272
310
|
let phase = null;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let
|
|
276
|
-
const settle = () => {
|
|
277
|
-
agent?.end();
|
|
278
|
-
agent = null;
|
|
279
|
-
phase = null;
|
|
280
|
-
};
|
|
311
|
+
const items = new Map();
|
|
312
|
+
let thread = 0;
|
|
313
|
+
let said = '';
|
|
281
314
|
const answers = new Set();
|
|
282
|
-
// What the person said in an event of its own. The harness prints their ask
|
|
283
|
-
// too (harness.ts); an event saying the same words is the one drawn. Words
|
|
284
|
-
// that steered the run are not its ask.
|
|
285
|
-
const told = new Set();
|
|
286
315
|
const steered = new Set();
|
|
287
316
|
const person = (text) => {
|
|
288
317
|
const c = { kind: 'said', key: mint(), who: 'person', text };
|
|
289
318
|
out.push(c);
|
|
290
|
-
told.add(c);
|
|
291
319
|
return c;
|
|
292
320
|
};
|
|
321
|
+
const failures = new Set();
|
|
322
|
+
const failed = (message) => {
|
|
323
|
+
if (failures.has(message))
|
|
324
|
+
return;
|
|
325
|
+
failures.add(message);
|
|
326
|
+
out.push({ kind: 'note', key: mint(), text: plain(message) });
|
|
327
|
+
if (message)
|
|
328
|
+
out.push({ kind: 'step', key: mint(), name: 'Error', detail: '', output: message, ran: 'error' });
|
|
329
|
+
};
|
|
293
330
|
for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
|
|
294
331
|
seq = e.seq;
|
|
295
332
|
n = 0;
|
|
296
333
|
const body = decode(e.payload);
|
|
297
334
|
const b = body && typeof body === 'object' ? body : null;
|
|
298
335
|
switch (e.kind) {
|
|
299
|
-
case '
|
|
300
|
-
|
|
301
|
-
if (!text)
|
|
336
|
+
case 'event': {
|
|
337
|
+
if (!b)
|
|
302
338
|
break;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
339
|
+
const type = str(b.type);
|
|
340
|
+
if (type === 'thread.started')
|
|
341
|
+
thread++;
|
|
342
|
+
if (type === 'error')
|
|
343
|
+
said = str(b.message);
|
|
344
|
+
if (type === 'turn.failed')
|
|
345
|
+
failed(str(obj(b.error).message) || said);
|
|
346
|
+
if (type !== 'item.started' && type !== 'item.updated' && type !== 'item.completed')
|
|
347
|
+
break;
|
|
348
|
+
const it = obj(b.item);
|
|
349
|
+
const id = str(it.id) && `${thread}:${str(it.id)}`;
|
|
350
|
+
const at = id ? items.get(id) : undefined;
|
|
351
|
+
const card = item(it, at === undefined ? mint() : out[at].key);
|
|
352
|
+
if (!card)
|
|
353
|
+
break;
|
|
354
|
+
if (at === undefined) {
|
|
355
|
+
if (id)
|
|
356
|
+
items.set(id, out.length);
|
|
357
|
+
out.push(card);
|
|
314
358
|
}
|
|
359
|
+
else
|
|
360
|
+
out[at] = card;
|
|
315
361
|
break;
|
|
316
362
|
}
|
|
317
363
|
case 'tool-call': {
|
|
@@ -321,20 +367,10 @@ export function cards(events, mode = '') {
|
|
|
321
367
|
const message = str(b.message);
|
|
322
368
|
if (step === 'exit') {
|
|
323
369
|
// One command in the sandbox ended: `exit 0`, `exit 1`, or `ended: …`.
|
|
324
|
-
if (
|
|
325
|
-
settle();
|
|
326
|
-
exited = true;
|
|
327
|
-
}
|
|
328
|
-
else if (phase && phase.ran === 'running')
|
|
370
|
+
if (phase && phase.ran === 'running')
|
|
329
371
|
phase.ran = message === 'exit 0' ? 'done' : 'error';
|
|
330
372
|
break;
|
|
331
373
|
}
|
|
332
|
-
if (message === TASK) {
|
|
333
|
-
settle();
|
|
334
|
-
exited = false;
|
|
335
|
-
agent = harness(out, mint);
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
374
|
if (step === 'leased') {
|
|
339
375
|
const lease = out.find((c) => c.kind === 'step' && c.name === NAMES.lease);
|
|
340
376
|
if (lease)
|
|
@@ -349,26 +385,33 @@ export function cards(events, mode = '') {
|
|
|
349
385
|
phase.detail = message;
|
|
350
386
|
break;
|
|
351
387
|
}
|
|
352
|
-
settle();
|
|
353
388
|
phase = { kind: 'step', key: mint(), name, detail: message, output: '', ran: ranOf(str(b.status)) };
|
|
354
389
|
out.push(phase);
|
|
355
390
|
break;
|
|
356
391
|
}
|
|
357
392
|
case 'status': {
|
|
358
|
-
|
|
393
|
+
phase = null;
|
|
359
394
|
if (!b)
|
|
360
395
|
break;
|
|
361
|
-
const
|
|
362
|
-
|
|
396
|
+
const done = str(b.status) === 'done';
|
|
397
|
+
const text = str(b.plan) || str(b.answer);
|
|
398
|
+
if (done && (mode === 'plan' || mode === 'setup') && text) {
|
|
363
399
|
answers.add(text.trim());
|
|
364
400
|
out.push(mode === 'plan' ? { kind: 'plan', key: mint(), text } : { kind: 'said', key: mint(), who: 'agent', text });
|
|
365
401
|
break;
|
|
366
402
|
}
|
|
403
|
+
const answer = str(b.answer).trim();
|
|
404
|
+
if (answer) {
|
|
405
|
+
answers.add(answer);
|
|
406
|
+
out.push({ kind: 'said', key: mint(), who: 'agent', text: answer });
|
|
407
|
+
}
|
|
408
|
+
if (str(b.status) === 'error') {
|
|
409
|
+
failed(str(b.error));
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
367
412
|
const line = status(b, mode);
|
|
368
413
|
if (line)
|
|
369
414
|
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' });
|
|
372
415
|
break;
|
|
373
416
|
}
|
|
374
417
|
case 'control': {
|
|
@@ -398,24 +441,21 @@ export function cards(events, mode = '') {
|
|
|
398
441
|
}
|
|
399
442
|
}
|
|
400
443
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
let kept = false;
|
|
444
|
+
// The answer a status carries is also the agent's last message: each is drawn once, as the answer.
|
|
445
|
+
const kept = new Set();
|
|
404
446
|
for (let i = out.length - 1; i >= 0; i--) {
|
|
405
447
|
const c = out[i];
|
|
406
448
|
if (!((c.kind === 'said' && c.who === 'agent') || c.kind === 'plan') || !answers.has(c.text.trim()))
|
|
407
449
|
continue;
|
|
408
|
-
if (kept)
|
|
450
|
+
if (kept.has(c.text.trim()))
|
|
409
451
|
out.splice(i, 1);
|
|
410
|
-
kept
|
|
452
|
+
kept.add(c.text.trim());
|
|
411
453
|
}
|
|
412
|
-
const heard = new Set([...told].map((c) => c.text.trim()));
|
|
413
|
-
const drawn = out.filter((c) => !(c.kind === 'said' && c.who === 'person' && !told.has(c) && heard.has(c.text.trim())));
|
|
414
454
|
// The ask opens the conversation, ahead of the steps that set the run up.
|
|
415
|
-
const ask =
|
|
455
|
+
const ask = out.findIndex((c) => c.kind === 'said' && c.who === 'person' && !steered.has(c));
|
|
416
456
|
if (ask > 0)
|
|
417
|
-
|
|
418
|
-
return
|
|
457
|
+
out.unshift(...out.splice(ask, 1));
|
|
458
|
+
return out;
|
|
419
459
|
}
|
|
420
460
|
/** The plan a plan run answered with, from its final status, or ''. */
|
|
421
461
|
export function answer(events) {
|
package/lib/desk.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ti
|
|
|
13
13
|
};
|
|
14
14
|
title: string;
|
|
15
15
|
project: string;
|
|
16
|
-
/** The sandbox the run
|
|
16
|
+
/** The sandbox the run leased, or '' before it leases one and on a machine. */
|
|
17
17
|
sandbox: string;
|
|
18
18
|
/** Where the run's work is published, or '' before it is. */
|
|
19
19
|
site: string;
|
package/lib/desk.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The pane beside a run's transcript, as tabs: the Browser showing
|
|
4
|
-
* is published, its sandbox's Desktop and
|
|
5
|
-
* what it pushed, and its codebase's
|
|
3
|
+
* The pane beside a run's transcript, as tabs: the Browser showing what the run's
|
|
4
|
+
* sandbox serves (or where the run is published), its sandbox's Desktop and
|
|
5
|
+
* Terminal, what it produced, its files, what it pushed, and its codebase's
|
|
6
|
+
* environment.
|
|
7
|
+
*
|
|
8
|
+
* The sandbox outlives the run: kept when the run ends, parked while nobody
|
|
9
|
+
* watches, retired a day after it parks. The bar under the tabs says which and
|
|
10
|
+
* suspends or resumes it; a tab that needs it running resumes it (sandbox.ts).
|
|
6
11
|
*/
|
|
7
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
8
13
|
import { ExternalLink, Info, MoreHorizontal, PanelRightClose, RefreshCcw } from '@hanzogui/lucide-icons-2';
|
|
9
14
|
import { Button, DropdownMenu } from '@hanzo/ui';
|
|
10
15
|
import { PreviewFrame } from '@hanzo/ui/agents';
|
|
11
|
-
import { useRef, useState } from 'react';
|
|
12
|
-
import {
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
17
|
+
import { park, ports as listening, preview, sandbox as look, wake } from './api/sandbox.js';
|
|
18
|
+
import { failure, shell } from './api/turn.js';
|
|
13
19
|
import { Door } from './door.js';
|
|
14
20
|
import { Environment } from './environment.js';
|
|
15
21
|
import { Artifacts, Files } from './files.js';
|
|
16
22
|
import { Git } from './git.js';
|
|
17
|
-
import {
|
|
23
|
+
import { useTarget } from './host.js';
|
|
24
|
+
import { away, Out } from './out.js';
|
|
18
25
|
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
19
26
|
const BLEED = ['browser', 'desktop', 'terminal'];
|
|
27
|
+
/** How often the sandbox's state, and what it serves, is read while the pane is open. */
|
|
28
|
+
const EVERY = 10_000;
|
|
29
|
+
const PORTS = 5_000;
|
|
20
30
|
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, site, published, events, live, menu, refused, retry, onRetry, onHide, }) {
|
|
21
31
|
// Until a tab is picked it follows the run: a setup run's work IS the
|
|
22
32
|
// environment, a published run opens on its site, any other on its terminal.
|
|
@@ -24,16 +34,179 @@ export function Desk({ id, repo, branch, base, environment, mode, pr, title, pro
|
|
|
24
34
|
const tab = picked ?? (mode === 'setup' ? 'environment' : site ? 'browser' : 'terminal');
|
|
25
35
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
26
36
|
const lines = shell(events);
|
|
37
|
+
const failed = failure(events);
|
|
27
38
|
const bleed = BLEED.includes(tab);
|
|
28
|
-
|
|
39
|
+
const box = useBox(sandbox);
|
|
40
|
+
// Nothing is said of the sandbox until it has been read.
|
|
41
|
+
const held = box.value?.state ?? '';
|
|
42
|
+
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') }] })] }), sandbox ? _jsx(Held, { box: box, live: live }) : null, _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: "Sandbox", value: sandbox || '—' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { sandbox: sandbox, held: held, 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, held: held, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, held: held, lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { session: id, sandbox: sandbox, 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: held === 'running' }) : null] })] }));
|
|
29
43
|
}
|
|
30
44
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
45
|
+
* The run's sandbox as it stands, and what moves it. It is read while the pane is
|
|
46
|
+
* open and the page is in view, and no more once it has ended or failed.
|
|
33
47
|
*/
|
|
34
|
-
function
|
|
48
|
+
function useBox(id) {
|
|
49
|
+
const t = useTarget();
|
|
50
|
+
const [value, setValue] = useState(null);
|
|
51
|
+
const [busy, setBusy] = useState(false);
|
|
52
|
+
const [note, setNote] = useState('');
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setValue(null);
|
|
55
|
+
if (!id)
|
|
56
|
+
return;
|
|
57
|
+
let gone = false;
|
|
58
|
+
let every;
|
|
59
|
+
const read = () => {
|
|
60
|
+
if (document.hidden)
|
|
61
|
+
return;
|
|
62
|
+
look(t, id).then((b) => {
|
|
63
|
+
if (gone)
|
|
64
|
+
return;
|
|
65
|
+
setValue(b);
|
|
66
|
+
if (b.state === 'gone' || b.state === 'error')
|
|
67
|
+
clearInterval(every);
|
|
68
|
+
}, (e) => !gone && setNote(e.message));
|
|
69
|
+
};
|
|
70
|
+
read();
|
|
71
|
+
every = setInterval(read, EVERY);
|
|
72
|
+
return () => {
|
|
73
|
+
gone = true;
|
|
74
|
+
clearInterval(every);
|
|
75
|
+
};
|
|
76
|
+
}, [t, id]);
|
|
77
|
+
const act = useCallback((how) => {
|
|
78
|
+
setBusy(true);
|
|
79
|
+
setNote('');
|
|
80
|
+
(how === 'park' ? park(t, id) : wake(t, id)).then(setValue, (e) => setNote(e.message)).finally(() => setBusy(false));
|
|
81
|
+
}, [t, id]);
|
|
82
|
+
return { value, busy, note, act };
|
|
83
|
+
}
|
|
84
|
+
/** When a parked sandbox is retired, as a person reads it. */
|
|
85
|
+
const until = (ms) => new Date(ms).toLocaleString(undefined, { weekday: 'short', hour: 'numeric', minute: '2-digit' });
|
|
86
|
+
/**
|
|
87
|
+
* The sandbox's line under the tabs: where it is, and the one thing to do to it.
|
|
88
|
+
* A sandbox a run is working in is not suspended from here.
|
|
89
|
+
*/
|
|
90
|
+
function Held({ box, live }) {
|
|
91
|
+
const b = box.value;
|
|
92
|
+
const state = b?.state ?? 'pending';
|
|
93
|
+
const says = state === 'running'
|
|
94
|
+
? 'Sandbox running'
|
|
95
|
+
: state === 'parked'
|
|
96
|
+
? b?.ends
|
|
97
|
+
? `Sandbox suspended · its files are kept until ${until(b.ends)}`
|
|
98
|
+
: 'Sandbox suspended · its files are kept'
|
|
99
|
+
: state === 'pending'
|
|
100
|
+
? 'Sandbox starting…'
|
|
101
|
+
: state === 'gone'
|
|
102
|
+
? 'Sandbox ended'
|
|
103
|
+
: `Sandbox unavailable${b?.error ? ` · ${b.error}` : ''}`;
|
|
104
|
+
return (_jsxs(XStack, { px: "$3", py: "$1.5", gap: "$2", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", "aria-label": "Sandbox", children: [_jsx(YStack, { width: 7, height: 7, rounded: 99, bg: state === 'running' ? '$green10' : state === 'parked' ? '$soft' : '$borderColor' }), _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, children: box.note || says }), state === 'running' && !live ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: box.busy, onPress: () => box.act('park'), children: "Suspend" })) : state === 'parked' ? (_jsx(Button, { size: "sm", variant: "outline", disabled: box.busy, onPress: () => box.act('wake'), children: box.busy ? 'Resuming…' : 'Resume' })) : null] }));
|
|
105
|
+
}
|
|
106
|
+
/** What a preview page says of itself, when it cannot serve. */
|
|
107
|
+
const PREVIEW = 'hanzo-preview';
|
|
108
|
+
/** How often a preview is opened again on its own before Retry is the person's to press. */
|
|
109
|
+
const REMINTS = 2;
|
|
110
|
+
const REMINT = 30_000;
|
|
111
|
+
/** What a preview page's status means to the person looking at it, when it is not reopened. */
|
|
112
|
+
const SAYS = {
|
|
113
|
+
401: () => 'This browser keeps the preview’s cookie out of a frame. Open it in a new tab.',
|
|
114
|
+
502: (port) => `Nothing answered on port ${port}. Start the server, then Retry.`,
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* The Browser tab: what the run's sandbox serves, one port at a time, framed at
|
|
118
|
+
* its own origin; or, with nothing serving, where the run is published. Only a
|
|
119
|
+
* running sandbox is shown: a suspended one is resumed from the bar, never by
|
|
120
|
+
* looking. A preview that lost its sandbox (409) is opened again, at most twice
|
|
121
|
+
* in a while; any other status is said, and Retry is the person's.
|
|
122
|
+
*/
|
|
123
|
+
function Browser({ sandbox, held, site, live }) {
|
|
124
|
+
const t = useTarget();
|
|
125
|
+
const [open, setOpen] = useState([]);
|
|
126
|
+
const [port, setPort] = useState(0);
|
|
127
|
+
const [src, setSrc] = useState('');
|
|
128
|
+
const [why, setWhy] = useState('');
|
|
129
|
+
const [attempt, setAttempt] = useState(0);
|
|
130
|
+
const reopened = useRef([]);
|
|
131
|
+
const frame = useRef(null);
|
|
132
|
+
const running = Boolean(sandbox) && held === 'running';
|
|
133
|
+
// What listens, read again while the sandbox runs: a dev server appears once it listens.
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!running) {
|
|
136
|
+
setOpen([]);
|
|
137
|
+
setPort(0);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let gone = false;
|
|
141
|
+
const read = () => listening(t, sandbox).then((ps) => {
|
|
142
|
+
if (gone)
|
|
143
|
+
return;
|
|
144
|
+
setOpen(ps);
|
|
145
|
+
setPort((p) => (ps.some((x) => x.port === p) ? p : (ps[0]?.port ?? 0)));
|
|
146
|
+
}, () => undefined);
|
|
147
|
+
void read();
|
|
148
|
+
const every = setInterval(read, PORTS);
|
|
149
|
+
return () => {
|
|
150
|
+
gone = true;
|
|
151
|
+
clearInterval(every);
|
|
152
|
+
};
|
|
153
|
+
}, [t, sandbox, running]);
|
|
154
|
+
// One address per port and attempt: its ticket is spent when the page opens it.
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
setSrc('');
|
|
157
|
+
setWhy('');
|
|
158
|
+
if (!port || !sandbox)
|
|
159
|
+
return;
|
|
160
|
+
let gone = false;
|
|
161
|
+
preview(t, sandbox, port).then((u) => !gone && setSrc(u), (e) => !gone && setWhy(e.message));
|
|
162
|
+
return () => {
|
|
163
|
+
gone = true;
|
|
164
|
+
};
|
|
165
|
+
}, [t, sandbox, port, attempt]);
|
|
166
|
+
// A preview that can no longer serve says so; one whose sandbox stopped is opened again, a little.
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!src)
|
|
169
|
+
return;
|
|
170
|
+
const origin = new URL(src).origin;
|
|
171
|
+
const hear = (e) => {
|
|
172
|
+
if (e.origin !== origin || e.source !== frame.current?.contentWindow)
|
|
173
|
+
return;
|
|
174
|
+
const m = e.data;
|
|
175
|
+
if (m?.source !== PREVIEW || typeof m.status !== 'number')
|
|
176
|
+
return;
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
reopened.current = reopened.current.filter((at) => now - at < REMINT);
|
|
179
|
+
if (m.status === 409 && reopened.current.length < REMINTS) {
|
|
180
|
+
reopened.current.push(now);
|
|
181
|
+
setAttempt((a) => a + 1);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const say = SAYS[m.status];
|
|
185
|
+
if (say || m.status === 409)
|
|
186
|
+
setWhy(say ? say(port) : 'The sandbox stopped serving. Retry to open it again.');
|
|
187
|
+
};
|
|
188
|
+
window.addEventListener('message', hear);
|
|
189
|
+
return () => window.removeEventListener('message', hear);
|
|
190
|
+
}, [src, port]);
|
|
191
|
+
const tab = () => away(preview(t, sandbox, port)).catch((e) => setWhy(e.message));
|
|
192
|
+
if (!running)
|
|
193
|
+
return _jsx(Site, { site: site, live: live, held: held });
|
|
194
|
+
if (!port)
|
|
195
|
+
return _jsx(Site, { site: site, live: live, held: held });
|
|
196
|
+
const host = open.find((p) => p.port === port)?.host ?? '';
|
|
197
|
+
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: () => setAttempt((a) => a + 1), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 14 }) }), open.length > 1 ? (_jsx(XStack, { gap: "$1", children: open.map((p) => (_jsx(XStack, { render: "button", "aria-label": `Port ${p.port}`, onPress: () => setPort(p.port), px: "$2", py: "$1", rounded: "$2", bg: p.port === port ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: p.port === port ? '$ink' : '$soft', children: String(p.port) }) }, p.port))) })) : null, _jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: host || `port ${port}` }), _jsx(XStack, { render: "button", "aria-label": "Open in a new tab", onPress: () => void tab(), p: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) })] }), why ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: why }), _jsxs(XStack, { gap: "$2", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: "Retry" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => void tab(), children: "Open in a new tab" })] })] })) : src ? (_jsx("iframe", { ref: frame, src: src, title: `What the run serves on port ${port}`, sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#fff' } })) : (_jsx(YStack, { flex: 1, items: "center", justify: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", children: `Opening port ${port}…` }) }))] }));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Where the run published its work, framed, with its address, a reload and the
|
|
201
|
+
* page in a tab of its own; or what keeps its sandbox from serving.
|
|
202
|
+
*/
|
|
203
|
+
function Site({ site, live, held }) {
|
|
35
204
|
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 || '
|
|
205
|
+
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 || 'Nothing is serving 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: "Nothing is serving yet" }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: held === 'parked'
|
|
206
|
+
? 'The run’s sandbox is suspended. Resume it to see what it serves.'
|
|
207
|
+
: live
|
|
208
|
+
? 'When the run starts a server in its sandbox, the page opens here.'
|
|
209
|
+
: 'Nothing listens in this run’s sandbox, and it published no site. Its work is on its branch.' })] }) })] }));
|
|
37
210
|
}
|
|
38
211
|
function TabButton({ id, tab, onPick, children }) {
|
|
39
212
|
const on = tab === id;
|
|
@@ -45,19 +218,18 @@ function Fact({ label, value }) {
|
|
|
45
218
|
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace: 'pre' };
|
|
46
219
|
/**
|
|
47
220
|
* The Terminal tab: a shell in the run's sandbox, in the same working tree as the
|
|
48
|
-
* agent, while the
|
|
49
|
-
*
|
|
50
|
-
* reopened tab reattaches to it.
|
|
221
|
+
* agent, while the sandbox is kept; and the agent's log, which outlives it. The
|
|
222
|
+
* shell is a tmux session named for the run, so a reopened tab reattaches to it.
|
|
51
223
|
*/
|
|
52
|
-
function Terminal({ id, sandbox, lines, live, refused, retry, onRetry, }) {
|
|
53
|
-
const reachable =
|
|
224
|
+
function Terminal({ id, sandbox, held, lines, live, failed, refused, retry, onRetry, }) {
|
|
225
|
+
const reachable = Boolean(sandbox) && held === 'running';
|
|
54
226
|
// The shell while there is one, unless the log was asked for.
|
|
55
227
|
const [log, setLog] = useState(false);
|
|
56
228
|
const showShell = reachable && !log;
|
|
57
|
-
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,
|
|
229
|
+
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, held: held, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })] }));
|
|
58
230
|
}
|
|
59
|
-
function Log({ lines, live, refused, retry, onRetry, }) {
|
|
231
|
+
function Log({ lines, live, failed, refused, retry, onRetry, }) {
|
|
60
232
|
const prompt = 'workspace';
|
|
61
233
|
const idle = lines.length === 0 && !live;
|
|
62
|
-
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." }) })] }));
|
|
234
|
+
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." }) })] }));
|
|
63
235
|
}
|
package/lib/door.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { type Door as Which } from './api/sandbox.ts';
|
|
2
|
-
|
|
1
|
+
import { type Door as Which, type State as Held } from './api/sandbox.ts';
|
|
2
|
+
/**
|
|
3
|
+
* `held` is where the run's sandbox is in its life, '' until it has been read. Only
|
|
4
|
+
* a running sandbox has an open door.
|
|
5
|
+
*/
|
|
6
|
+
export declare function Door({ which, sandbox, held, session }: {
|
|
3
7
|
which: Which;
|
|
4
8
|
sandbox: string;
|
|
5
|
-
|
|
9
|
+
held: Held | '';
|
|
6
10
|
session: string;
|
|
7
11
|
}): import("react").JSX.Element;
|