@hanzo/build 0.2.16 → 0.2.18
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/codebases.d.ts +1 -1
- package/lib/api/codebases.js +3 -3
- package/lib/api/harness.d.ts +7 -0
- package/lib/api/harness.js +34 -6
- package/lib/api/sessions.d.ts +2 -2
- package/lib/api/sessions.js +1 -1
- package/lib/api/turn.js +37 -16
- package/lib/ask.js +2 -2
- package/lib/builder.js +36 -4
- package/lib/choice.js +2 -2
- package/lib/customize/agents.js +4 -4
- package/lib/customize/connectors.js +7 -8
- package/lib/customize/index.js +3 -2
- package/lib/customize/plugins.js +3 -3
- package/lib/customize/skills.js +4 -4
- package/lib/customize/ui.d.ts +6 -2
- package/lib/customize/ui.js +9 -3
- package/lib/data.js +17 -15
- package/lib/desk.d.ts +4 -4
- package/lib/desk.js +6 -4
- package/lib/door.js +1 -1
- package/lib/environment.js +3 -3
- package/lib/find.js +2 -2
- package/lib/forge.js +6 -14
- package/lib/git.js +1 -1
- package/lib/host.d.ts +2 -0
- package/lib/landing.js +25 -16
- package/lib/markdown.js +6 -4
- package/lib/pitch.d.ts +4 -0
- package/lib/pitch.js +39 -0
- package/lib/plans.js +10 -9
- package/lib/project.js +20 -21
- package/lib/prose.js +1 -1
- package/lib/publish.js +9 -8
- package/lib/run.js +72 -45
- package/lib/settings/account.js +7 -5
- package/lib/settings/billing.js +11 -20
- package/lib/settings/capabilities.js +1 -1
- package/lib/settings/card.js +2 -2
- package/lib/settings/code.js +6 -3
- package/lib/settings/general.js +6 -5
- package/lib/settings/index.js +2 -3
- package/lib/settings/integrations.js +2 -2
- package/lib/settings/keys.js +8 -9
- package/lib/settings/machines.js +5 -9
- package/lib/settings/members.js +3 -2
- package/lib/settings/memory.js +1 -1
- package/lib/settings/notifications.js +1 -1
- package/lib/settings/privacy.js +2 -2
- package/lib/settings/usage.js +8 -9
- package/lib/shelf.js +5 -9
- package/lib/sync.js +2 -5
- package/lib/tools.js +2 -3
- package/lib/transcript.d.ts +5 -2
- package/lib/transcript.js +21 -3
- package/lib/visit.d.ts +31 -0
- package/lib/visit.js +53 -0
- package/lib/voice.js +3 -5
- package/package.json +1 -1
package/lib/api/codebases.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare function chosen(r: {
|
|
|
41
41
|
full_name?: string;
|
|
42
42
|
private?: boolean;
|
|
43
43
|
default_branch?: string;
|
|
44
|
-
}
|
|
44
|
+
}): ForgeRepo;
|
|
45
45
|
/** The chip's row for a codebase the forge listed. */
|
|
46
46
|
export declare function asRepo(c: Codebase): ForgeRepo;
|
|
47
47
|
/** Provision an empty repository. The name is the handle and the last path segment. */
|
package/lib/api/codebases.js
CHANGED
|
@@ -30,7 +30,7 @@ export function codebase(raw, org = '') {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
/** A chip row remembered as a forge codebase, when the list had not seen it yet. */
|
|
33
|
-
export function chosen(r
|
|
33
|
+
export function chosen(r) {
|
|
34
34
|
return {
|
|
35
35
|
owner: r.owner,
|
|
36
36
|
name: r.name,
|
|
@@ -40,7 +40,7 @@ export function chosen(r, clone = '') {
|
|
|
40
40
|
pushed_at: '',
|
|
41
41
|
installation_id: 0,
|
|
42
42
|
forge: true,
|
|
43
|
-
clone,
|
|
43
|
+
clone: '',
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
/** The chip's row for a codebase the forge listed. */
|
|
@@ -51,7 +51,7 @@ export function asRepo(c) {
|
|
|
51
51
|
name: c.name,
|
|
52
52
|
full_name: owner ? `${owner}/${c.name}` : c.name,
|
|
53
53
|
private: !c.public,
|
|
54
|
-
default_branch: c.branch
|
|
54
|
+
default_branch: c.branch,
|
|
55
55
|
pushed_at: c.updated,
|
|
56
56
|
installation_id: 0,
|
|
57
57
|
forge: true,
|
package/lib/api/harness.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* run's session as `log` chunks (apps/sandbox work.go). It prints each item of
|
|
6
6
|
* its turn under a header line (hanzo dev, exec/src/event_processor_with_human_output.rs):
|
|
7
7
|
*
|
|
8
|
+
* user / <task> what the agent was asked
|
|
8
9
|
* exec / <command> in <dir> a command started
|
|
9
10
|
* succeeded in 12ms: / <output> …how it ended, then what it printed
|
|
10
11
|
* codex / <text> the agent said something
|
|
@@ -18,6 +19,10 @@
|
|
|
18
19
|
* is whole. Lines no header owns — reasoning, or the tail of output from before
|
|
19
20
|
* the recorded events begin — are prose. Everything here becomes TEXT on the
|
|
20
21
|
* screen; nothing is markup.
|
|
22
|
+
*
|
|
23
|
+
* The task under `user` is what the person asked, with what the platform told
|
|
24
|
+
* the agent ahead of it and what a follow-up said after it taken off (`asked`),
|
|
25
|
+
* so it opens the transcript as the person's own words.
|
|
21
26
|
*/
|
|
22
27
|
import type { Card } from './turn.ts';
|
|
23
28
|
/**
|
|
@@ -34,6 +39,8 @@ export declare function unwrap(command: string): string;
|
|
|
34
39
|
export declare function reads(command: string): string;
|
|
35
40
|
/** A path to show: a relative one as it is, an absolute one by its name — never the sandbox's layout. */
|
|
36
41
|
export declare const shown: (path: string) => string;
|
|
42
|
+
/** What the person asked, out of the task the agent was given. A follow-up with nothing said keeps its own line. */
|
|
43
|
+
export declare function asked(task: string): string;
|
|
37
44
|
/** A unified diff's sections, by the path each changes, from its first hunk on. */
|
|
38
45
|
export declare function sections(diff: string): Map<string, string>;
|
|
39
46
|
/** A reader that appends the cards it finds to `out`, keyed by `mint`. */
|
package/lib/api/harness.js
CHANGED
|
@@ -89,6 +89,23 @@ export const shown = (path) => {
|
|
|
89
89
|
const cut = path.replace(/\/+$/, '');
|
|
90
90
|
return cut.slice(cut.lastIndexOf('/') + 1);
|
|
91
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* The paragraphs the platform puts ahead of a task — the ask_user line and the
|
|
94
|
+
* environment brief (apps/coding sandboxrunner.go), a plan's or a setup's
|
|
95
|
+
* instructions and the setup's answer block (mode.go) — and the one a follow-up
|
|
96
|
+
* puts after the person's words (coding.ts `followUp`). None is theirs.
|
|
97
|
+
*/
|
|
98
|
+
const AHEAD = /^(When a decision needs the person you are working for, call ask_user|These environment variables are named on this codebase|This codebase's (install script|start command)|Plan only\. Read the repository|Set up this repository's development environment\.|```environment\n)/;
|
|
99
|
+
const AFTER = /^(This follows an earlier run on this codebase|An earlier run already worked on this;|Continue where the earlier run left off\.)/;
|
|
100
|
+
/** What the person asked, out of the task the agent was given. A follow-up with nothing said keeps its own line. */
|
|
101
|
+
export function asked(task) {
|
|
102
|
+
const parts = task.trim().split(/\n\s*\n/);
|
|
103
|
+
while (parts.length && AHEAD.test(parts[0]))
|
|
104
|
+
parts.shift();
|
|
105
|
+
if (parts.length > 1 && AFTER.test(parts[parts.length - 1]))
|
|
106
|
+
parts.pop();
|
|
107
|
+
return parts.join('\n\n');
|
|
108
|
+
}
|
|
92
109
|
/** A unified diff's sections, by the path each changes, from its first hunk on. */
|
|
93
110
|
export function sections(diff) {
|
|
94
111
|
const out = new Map();
|
|
@@ -126,6 +143,8 @@ export function harness(out, mint) {
|
|
|
126
143
|
const edits = [];
|
|
127
144
|
let edit = null;
|
|
128
145
|
let diff = null;
|
|
146
|
+
// Each task the agent was given, as the person's words once the whole block is read.
|
|
147
|
+
const tasks = [];
|
|
129
148
|
const said = (text) => {
|
|
130
149
|
const c = { kind: 'said', key: mint(), who: 'agent', text };
|
|
131
150
|
out.push(c);
|
|
@@ -177,6 +196,11 @@ export function harness(out, mint) {
|
|
|
177
196
|
diff.push(l);
|
|
178
197
|
return;
|
|
179
198
|
}
|
|
199
|
+
// A task is the person's words to the next header, whatever they look like.
|
|
200
|
+
if (mode === 'user' && !HEAD.test(l)) {
|
|
201
|
+
tasks[tasks.length - 1].lines.push(l);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
180
204
|
const result = RESULT.exec(l);
|
|
181
205
|
if (result && open.length) {
|
|
182
206
|
settle();
|
|
@@ -198,9 +222,13 @@ export function harness(out, mint) {
|
|
|
198
222
|
card = said('');
|
|
199
223
|
mode = 'agent';
|
|
200
224
|
return;
|
|
201
|
-
case 'user':
|
|
225
|
+
case 'user': {
|
|
226
|
+
const c = { kind: 'said', key: mint(), who: 'person', text: '' };
|
|
227
|
+
out.push(c);
|
|
228
|
+
tasks.push({ card: c, lines: [] });
|
|
202
229
|
mode = 'user';
|
|
203
230
|
return;
|
|
231
|
+
}
|
|
204
232
|
case 'apply patch':
|
|
205
233
|
patch();
|
|
206
234
|
mode = 'prose';
|
|
@@ -275,15 +303,12 @@ export function harness(out, mint) {
|
|
|
275
303
|
card.output += `\n${row}`;
|
|
276
304
|
else {
|
|
277
305
|
card = step('Plan', '', 'done');
|
|
278
|
-
|
|
279
|
-
card.output = row;
|
|
306
|
+
card.output = row;
|
|
280
307
|
}
|
|
281
308
|
mode = 'prose';
|
|
282
309
|
return;
|
|
283
310
|
}
|
|
284
311
|
switch (mode) {
|
|
285
|
-
case 'user':
|
|
286
|
-
return;
|
|
287
312
|
case 'patch':
|
|
288
313
|
if (l.trim() && edit)
|
|
289
314
|
edit.paths.push(l.trim());
|
|
@@ -310,7 +335,7 @@ export function harness(out, mint) {
|
|
|
310
335
|
feed(chunk) {
|
|
311
336
|
const text = rest + chunk;
|
|
312
337
|
const lines = text.split('\n');
|
|
313
|
-
rest = lines.pop()
|
|
338
|
+
rest = lines.pop();
|
|
314
339
|
for (const l of lines)
|
|
315
340
|
line(l);
|
|
316
341
|
},
|
|
@@ -338,6 +363,9 @@ export function harness(out, mint) {
|
|
|
338
363
|
e.card.patch = mine.length === 1 ? parts.get(mine[0]) : mine.map((f) => `${f}\n${parts.get(f)}`).join('\n');
|
|
339
364
|
}
|
|
340
365
|
edits.length = 0;
|
|
366
|
+
for (const k of tasks)
|
|
367
|
+
k.card.text = asked(k.lines.join('\n'));
|
|
368
|
+
tasks.length = 0;
|
|
341
369
|
for (let i = out.length - 1; i >= 0; i--) {
|
|
342
370
|
const c = out[i];
|
|
343
371
|
if (c.kind === 'said' && !c.text.trim())
|
package/lib/api/sessions.d.ts
CHANGED
|
@@ -97,8 +97,8 @@ export declare function rename(t: Target, id: string, title: string): Promise<Se
|
|
|
97
97
|
* the org and the project.
|
|
98
98
|
*/
|
|
99
99
|
export declare function publish(t: Target, id: string, on: boolean): Promise<Session>;
|
|
100
|
-
/** Where anyone can read a published run's story: the public build route, by org and project. */
|
|
101
|
-
export declare const story: (t: Target, org: string, project: string) => string;
|
|
100
|
+
/** Where anyone can read a published run's story: the public build route, by org and project. '' without both. */
|
|
101
|
+
export declare const story: (t: Target, org: string | null, project: string) => string;
|
|
102
102
|
export interface Watch {
|
|
103
103
|
session?: (s: Session) => void;
|
|
104
104
|
event?: (e: Event) => void;
|
package/lib/api/sessions.js
CHANGED
|
@@ -122,7 +122,7 @@ export async function rename(t, id, title) {
|
|
|
122
122
|
export async function publish(t, id, on) {
|
|
123
123
|
return session(await call(t, 'PATCH', `/v1/agent/sessions/${seg(id)}`, { published: on }));
|
|
124
124
|
}
|
|
125
|
-
/** Where anyone can read a published run's story: the public build route, by org and project. */
|
|
125
|
+
/** Where anyone can read a published run's story: the public build route, by org and project. '' without both. */
|
|
126
126
|
export const story = (t, org, project) => org && project ? `${t.api}/v1/agent/builds/${seg(org)}/${seg(project)}` : '';
|
|
127
127
|
/**
|
|
128
128
|
* Follow one run's tree until `signal` aborts. Reconnects with a capped backoff;
|
package/lib/api/turn.js
CHANGED
|
@@ -29,15 +29,13 @@ export function decode(payload) {
|
|
|
29
29
|
const text = payload.trim();
|
|
30
30
|
if (!text.startsWith('{'))
|
|
31
31
|
return payload;
|
|
32
|
+
// JSON that opens with a brace is an object; anything else is prose that opens with one.
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
-
if (parsed && typeof parsed === 'object')
|
|
35
|
-
return parsed;
|
|
34
|
+
return JSON.parse(text);
|
|
36
35
|
}
|
|
37
36
|
catch {
|
|
38
|
-
|
|
37
|
+
return payload;
|
|
39
38
|
}
|
|
40
|
-
return payload;
|
|
41
39
|
}
|
|
42
40
|
/** A file's name, never the host path it sat at. */
|
|
43
41
|
const base = (path) => {
|
|
@@ -202,7 +200,7 @@ export function who(actor) {
|
|
|
202
200
|
const cut = actor.indexOf('/');
|
|
203
201
|
if (cut === -1)
|
|
204
202
|
return actor.split('-')[0] || actor;
|
|
205
|
-
const head = actor.slice(cut + 1).split('-')[0]
|
|
203
|
+
const head = actor.slice(cut + 1).split('-')[0];
|
|
206
204
|
return head ? `${actor.slice(0, cut)}/${head}` : actor;
|
|
207
205
|
}
|
|
208
206
|
/**
|
|
@@ -260,6 +258,17 @@ export function cards(events, mode = '') {
|
|
|
260
258
|
phase = null;
|
|
261
259
|
};
|
|
262
260
|
const answers = new Set();
|
|
261
|
+
// What the person said in an event of its own. The harness prints their ask
|
|
262
|
+
// too (harness.ts); an event saying the same words is the one drawn. Words
|
|
263
|
+
// that steered the run are not its ask.
|
|
264
|
+
const told = new Set();
|
|
265
|
+
const steered = new Set();
|
|
266
|
+
const person = (text) => {
|
|
267
|
+
const c = { kind: 'said', key: mint(), who: 'person', text };
|
|
268
|
+
out.push(c);
|
|
269
|
+
told.add(c);
|
|
270
|
+
return c;
|
|
271
|
+
};
|
|
263
272
|
for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
|
|
264
273
|
seq = e.seq;
|
|
265
274
|
n = 0;
|
|
@@ -270,16 +279,18 @@ export function cards(events, mode = '') {
|
|
|
270
279
|
const text = typeof body === 'string' ? body : str(b?.message) || str(b?.text);
|
|
271
280
|
if (!text)
|
|
272
281
|
break;
|
|
273
|
-
if (!agent && !phase && exited) {
|
|
274
|
-
phase = { kind: 'step', key: mint(), name: 'Commit', detail: 'committing the work', output: '', ran: 'running' };
|
|
275
|
-
out.push(phase);
|
|
276
|
-
}
|
|
277
|
-
if (!agent && !phase)
|
|
278
|
-
agent = harness(out, mint);
|
|
279
282
|
if (agent)
|
|
280
283
|
agent.feed(text);
|
|
281
284
|
else if (phase)
|
|
282
285
|
phase.output += text;
|
|
286
|
+
else if (exited) {
|
|
287
|
+
phase = { kind: 'step', key: mint(), name: 'Commit', detail: 'committing the work', output: text, ran: 'running' };
|
|
288
|
+
out.push(phase);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
agent = harness(out, mint);
|
|
292
|
+
agent.feed(text);
|
|
293
|
+
}
|
|
283
294
|
break;
|
|
284
295
|
}
|
|
285
296
|
case 'tool-call': {
|
|
@@ -343,7 +354,7 @@ export function cards(events, mode = '') {
|
|
|
343
354
|
const command = str(b.command);
|
|
344
355
|
const said = str(b.message);
|
|
345
356
|
if (command === 'message' && said)
|
|
346
|
-
|
|
357
|
+
steered.add(person(said));
|
|
347
358
|
else if (command === 'pause')
|
|
348
359
|
out.push({ kind: 'note', key: mint(), text: 'Pause asked' });
|
|
349
360
|
else if (command === 'resume')
|
|
@@ -354,8 +365,12 @@ export function cards(events, mode = '') {
|
|
|
354
365
|
}
|
|
355
366
|
case 'message': {
|
|
356
367
|
const text = typeof body === 'string' ? body : str(b?.text) || str(b?.content) || str(b?.message);
|
|
357
|
-
if (text)
|
|
358
|
-
|
|
368
|
+
if (!text)
|
|
369
|
+
break;
|
|
370
|
+
if (str(b?.role) === 'user')
|
|
371
|
+
person(text);
|
|
372
|
+
else
|
|
373
|
+
out.push({ kind: 'said', key: mint(), who: 'agent', text });
|
|
359
374
|
break;
|
|
360
375
|
}
|
|
361
376
|
}
|
|
@@ -371,7 +386,13 @@ export function cards(events, mode = '') {
|
|
|
371
386
|
out.splice(i, 1);
|
|
372
387
|
kept = true;
|
|
373
388
|
}
|
|
374
|
-
|
|
389
|
+
const heard = new Set([...told].map((c) => c.text.trim()));
|
|
390
|
+
const drawn = out.filter((c) => !(c.kind === 'said' && c.who === 'person' && !told.has(c) && heard.has(c.text.trim())));
|
|
391
|
+
// The ask opens the conversation, ahead of the steps that set the run up.
|
|
392
|
+
const ask = drawn.findIndex((c) => c.kind === 'said' && c.who === 'person' && !steered.has(c));
|
|
393
|
+
if (ask > 0)
|
|
394
|
+
drawn.unshift(...drawn.splice(ask, 1));
|
|
395
|
+
return drawn;
|
|
375
396
|
}
|
|
376
397
|
/** The plan a plan run answered with, from its final status, or ''. */
|
|
377
398
|
export function answer(events) {
|
package/lib/ask.js
CHANGED
|
@@ -20,7 +20,7 @@ function useAct(onDone) {
|
|
|
20
20
|
onDone();
|
|
21
21
|
}
|
|
22
22
|
catch (e) {
|
|
23
|
-
setNote(e
|
|
23
|
+
setNote(e.message);
|
|
24
24
|
setWorking(false);
|
|
25
25
|
}
|
|
26
26
|
};
|
|
@@ -63,7 +63,7 @@ export function Rename({ open, onOpenChange, title, name, run, }) {
|
|
|
63
63
|
void go(() => run(next));
|
|
64
64
|
};
|
|
65
65
|
return (_jsx(Dialog, { open: open, onOpenChange: close, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: title }), _jsx(Input, { autoFocus: true, value: value, onChangeText: setValue, "aria-label": "Name", onKeyDown: (e) => {
|
|
66
|
-
if (
|
|
66
|
+
if (e.key === 'Enter')
|
|
67
67
|
save();
|
|
68
68
|
} }), _jsx(Said, { note: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => close(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: save, children: "Save" })] })] }) }));
|
|
69
69
|
}
|
package/lib/builder.js
CHANGED
|
@@ -32,7 +32,7 @@ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, M
|
|
|
32
32
|
import { Button } from '@hanzo/ui';
|
|
33
33
|
import { SessionRail } from '@hanzo/ui/chat';
|
|
34
34
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
35
|
-
import { useMemo, useState } from 'react';
|
|
35
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
36
36
|
import { pinBoard } from './choice.js';
|
|
37
37
|
import { Customize } from './customize/index.js';
|
|
38
38
|
import { useKept, useRecents } from './data.js';
|
|
@@ -49,6 +49,8 @@ import { Artifacts, Templates } from './shelf.js';
|
|
|
49
49
|
import { Find } from './find.js';
|
|
50
50
|
import { DOCS, Slack, Who } from './foot.js';
|
|
51
51
|
import { Settings } from './settings/index.js';
|
|
52
|
+
import { Pitch } from './pitch.js';
|
|
53
|
+
import { Bar } from './visit.js';
|
|
52
54
|
function Shell() {
|
|
53
55
|
const host = useHost();
|
|
54
56
|
const t = useTarget();
|
|
@@ -75,7 +77,7 @@ function Shell() {
|
|
|
75
77
|
// it the search that finds a run. The organization is the account's, so it is
|
|
76
78
|
// switched from the account menu at the foot. Beside the rail from md up, beside
|
|
77
79
|
// the drawer's button below it — one place at a time.
|
|
78
|
-
const name =
|
|
80
|
+
const name = _jsx(Name, { onPress: () => go('') });
|
|
79
81
|
const links = [
|
|
80
82
|
{ id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
|
|
81
83
|
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
@@ -115,6 +117,31 @@ function Shell() {
|
|
|
115
117
|
}
|
|
116
118
|
: { name: 'Sign in', onPress: () => host.signIn?.() }, collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 92, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(YStack, { position: "absolute", t: 10, l: 8, r: 8, z: 2, gap: "$2.5", "$max-md": { display: 'none' }, children: [_jsx(XStack, { px: "$2.5", children: name }), _jsxs(XStack, { render: "button", "aria-label": "Search runs", onPress: () => setFinding(true), items: "center", gap: "$2", px: "$2.5", height: 34, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 15, color: "$soft" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Search" })] })] })), host.person ? _jsx(Who, { open: menu, onOpenChange: setMenu, narrow: collapsed }) : null] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 44, px: "$2", gap: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }), name, _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setFinding(true), "aria-label": "Search runs", children: _jsx(Search, { size: 18 }) })] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
|
|
117
119
|
}
|
|
120
|
+
/** The wordmark: the product's name, as its host calls it, which leads to New. */
|
|
121
|
+
function Name({ onPress }) {
|
|
122
|
+
const { name } = useHost();
|
|
123
|
+
return (_jsx(XStack, { render: "button", "aria-label": name, items: "center", onPress: onPress, children: _jsx(SizableText, { size: "$5", fontWeight: "600", color: "$ink", numberOfLines: 1, children: name }) }));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A visitor: no runs to list, so the product's header stands where the rail
|
|
127
|
+
* would. On New the composer fills the first screen and the pitch follows it.
|
|
128
|
+
*/
|
|
129
|
+
function Visit() {
|
|
130
|
+
const host = useHost();
|
|
131
|
+
const r = route(host.path);
|
|
132
|
+
// An address that names a part of the pitch (/#features) opens scrolled to it.
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const id = window.location.hash.slice(1);
|
|
135
|
+
if (id)
|
|
136
|
+
document.getElementById(id)?.scrollIntoView();
|
|
137
|
+
}, []);
|
|
138
|
+
const start = () => {
|
|
139
|
+
const page = document.getElementById('visit');
|
|
140
|
+
page?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
141
|
+
page?.querySelector('textarea, [contenteditable="true"], input[type="text"]')?.focus();
|
|
142
|
+
};
|
|
143
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsx(Bar, { name: _jsx(Name, { onPress: () => host.go('') }) }), r.kind === 'new' ? (_jsxs(YStack, { id: "visit", flex: 1, minH: 0, overflow: "scroll", children: [_jsx(YStack, { shrink: 0, style: { height: 'calc(100dvh - 60px)' }, children: _jsx(Pane, {}) }), _jsx(Pitch, { onStart: start })] })) : (_jsx(Pane, {}))] }));
|
|
144
|
+
}
|
|
118
145
|
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
119
146
|
function Pane({ onStarted }) {
|
|
120
147
|
const host = useHost();
|
|
@@ -143,7 +170,7 @@ function Pane({ onStarted }) {
|
|
|
143
170
|
function Brand({ org }) {
|
|
144
171
|
if (!org || org === 'hanzo')
|
|
145
172
|
return _jsx(HanzoMark, { size: 18 });
|
|
146
|
-
const letter = org.
|
|
173
|
+
const letter = org.charAt(0).toUpperCase();
|
|
147
174
|
return (_jsx(XStack, { width: 18, height: 18, rounded: "$1", items: "center", justify: "center", bg: "$raised", "aria-hidden": true, children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: letter }) }));
|
|
148
175
|
}
|
|
149
176
|
function Screens({ rail }) {
|
|
@@ -151,7 +178,12 @@ function Screens({ rail }) {
|
|
|
151
178
|
const r = route(host.path);
|
|
152
179
|
if (r.kind === 'project')
|
|
153
180
|
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
154
|
-
|
|
181
|
+
if (!rail)
|
|
182
|
+
return _jsx(Pane, {});
|
|
183
|
+
// Signed out with no token to wait on: a token still resolving is a person, not a visitor.
|
|
184
|
+
if (!host.person && !host.token() && host.signIn)
|
|
185
|
+
return _jsx(Visit, {});
|
|
186
|
+
return _jsx(Shell, {});
|
|
155
187
|
}
|
|
156
188
|
/**
|
|
157
189
|
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
package/lib/choice.js
CHANGED
|
@@ -29,7 +29,7 @@ function write(org, value) {
|
|
|
29
29
|
/** Point New at a forge codebase, and optionally a first draft. */
|
|
30
30
|
export function pinCodebase(org, c, ask = '') {
|
|
31
31
|
const repo = asRepo(c);
|
|
32
|
-
write(org, { ...read(org), repo, branch: repo.default_branch
|
|
32
|
+
write(org, { ...read(org), repo, branch: repo.default_branch, ask });
|
|
33
33
|
}
|
|
34
34
|
/** Which board Issues is showing. '' is every board. */
|
|
35
35
|
export function pinBoard(org, key) {
|
|
@@ -39,7 +39,7 @@ export function pinBoard(org, key) {
|
|
|
39
39
|
catch {
|
|
40
40
|
/* Issues then shows every board */
|
|
41
41
|
}
|
|
42
|
-
window.dispatchEvent(new
|
|
42
|
+
window.dispatchEvent(new CustomEvent('hanzo-board', { detail: key }));
|
|
43
43
|
}
|
|
44
44
|
export const isForge = (repo) => Boolean(repo && repo.forge === true && repo.name);
|
|
45
45
|
const syncingKey = (org) => `hanzo.build.sync.${org ?? 'none'}`;
|
package/lib/customize/agents.js
CHANGED
|
@@ -20,10 +20,10 @@ import { label as named, models } from '../api/models.js';
|
|
|
20
20
|
import { tools } from '../api/tools.js';
|
|
21
21
|
import { useRead } from '../data.js';
|
|
22
22
|
import { useHost, useTarget } from '../host.js';
|
|
23
|
-
import { Add, Choice, Confirm, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile } from './ui.js';
|
|
23
|
+
import { Add, Choice, Confirm, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile, Visitor } from './ui.js';
|
|
24
24
|
/** The model a new agent runs on when none is chosen: the deployment's own default. */
|
|
25
25
|
const DEFAULT = 'default';
|
|
26
|
-
const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')
|
|
26
|
+
const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')}`;
|
|
27
27
|
export function Agents({ view, q, adding, onAdding, onView }) {
|
|
28
28
|
const host = useHost();
|
|
29
29
|
const t = useTarget();
|
|
@@ -57,7 +57,7 @@ export function Agents({ view, q, adding, onAdding, onView }) {
|
|
|
57
57
|
return (_jsxs(YStack, { gap: "$3", children: [offered.error && !offered.value.length ? (_jsx(Soft, { children: offered.error.message })) : offered.loading && !offered.value.length ? (_jsx(Soft, { children: "Reading the presets\u2026" })) : !shown.length ? (_jsx(Soft, { children: offered.value.length ? 'No preset matches.' : 'The platform offers no presets.' })) : (_jsxs(_Fragment, { children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Presets from the platform. Adding one opens a new agent written from it, to change before it is saved." }), _jsx(Grid, { label: "Presets", children: shown.map((p) => (_jsx(Tile, { title: p.title || p.id, detail: p.prompt, meta: `Preset ${p.id}`, mark: _jsx(Mark, { name: p.title || p.id, icon: _jsx(Bot, { size: 15 }) }), action: signed ? _jsx(Add, { name: p.title || p.id, added: names.has(p.id), onPress: () => setStart(fromPreset(p)) }) : undefined }, p.id))) })] })), editor] }));
|
|
58
58
|
}
|
|
59
59
|
if (!signed) {
|
|
60
|
-
return _jsx(
|
|
60
|
+
return _jsx(Visitor, { children: "Sign in to see your agents." });
|
|
61
61
|
}
|
|
62
62
|
const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
|
|
63
63
|
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Soft, { children: mine.error.message })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Reading your agents\u2026" })) : !mine.value.length ? (_jsx(Soft, { action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children: "New agent" }), children: "No agents yet. Give one a model, instructions and the tools it may call." })) : !shown.length ? (_jsx(Soft, { children: "No agent matches." })) : (_jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Mark, { name: a.name, icon: a.emoji ? _jsx(SizableText, { size: "$4", children: a.emoji }) : undefined }), onOpen: () => setOpened(a) }, a.id || a.name))) })), editor] }));
|
|
@@ -111,7 +111,7 @@ function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
|
|
|
111
111
|
onSaved(saved);
|
|
112
112
|
}
|
|
113
113
|
catch (e) {
|
|
114
|
-
setNote(e
|
|
114
|
+
setNote(e.message);
|
|
115
115
|
}
|
|
116
116
|
finally {
|
|
117
117
|
setWorking(false);
|
|
@@ -20,7 +20,7 @@ import { toggle, tools } from '../api/tools.js';
|
|
|
20
20
|
import { useRead } from '../data.js';
|
|
21
21
|
import { useHost, useTarget } from '../host.js';
|
|
22
22
|
import { Out } from '../out.js';
|
|
23
|
-
import { Add, Confirm, day, Featured, Field, Grid, left, Line, Mark, matches, mono, Part, Sheet, Soft, Tile } from './ui.js';
|
|
23
|
+
import { Add, Confirm, day, Featured, Field, Grid, left, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, Visitor } from './ui.js';
|
|
24
24
|
const PAGE = 48;
|
|
25
25
|
/** The part of a URL a person reads: its host and path. */
|
|
26
26
|
const where = (url) => url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
@@ -53,7 +53,7 @@ export function Connectors({ view, q, adding, onAdding, onView }) {
|
|
|
53
53
|
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Line, { children: note }), _jsx(Shelf, { q: q, signed: signed, may: may, added: new Set(mine.value.map((s) => s.listing).filter(Boolean)), onOpen: setListing, onAdd: setConnecting }), _jsx(Natives, { q: q }), detail, connect] }));
|
|
54
54
|
}
|
|
55
55
|
if (!signed) {
|
|
56
|
-
return _jsx(
|
|
56
|
+
return _jsx(Visitor, { children: "Sign in to see your connectors." });
|
|
57
57
|
}
|
|
58
58
|
const shown = mine.value.filter((s) => matches(q, s.name, s.url, s.listing));
|
|
59
59
|
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Soft, { children: mine.error.message })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Reading your connectors\u2026" })) : !mine.value.length ? (_jsx(Soft, { action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Discover connectors" }), children: may ? 'No connectors yet. Add an MCP server by its URL, or pick one off the shelf.' : 'No connectors yet. An org admin adds them.' })) : !shown.length ? (_jsx(Soft, { children: "No connector matches." })) : (_jsx(Grid, { label: "Your connectors", children: shown.map((s) => (_jsx(Tile, { title: s.name, detail: where(s.url), meta: [s.listing ? 'From the shelf' : 'Added by URL', s.secret ? 'secret sealed in KMS' : '', day(s.created), s.admitted ? '' : 'not in runs until an admin adds it again'].filter(Boolean).join(' · '), mark: _jsx(Mark, { name: s.name }), onOpen: () => setOpened(s) }, s.id))) })), opened ? (_jsx(Detail, { server: opened, may: may, onClose: () => setOpened(null), onDeleted: () => {
|
|
@@ -65,7 +65,6 @@ export function Connectors({ view, q, adding, onAdding, onView }) {
|
|
|
65
65
|
/** The shelf: featured first, then by name, a page at a time. The platform searches it. */
|
|
66
66
|
function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
67
67
|
const t = useTarget();
|
|
68
|
-
const host = useHost();
|
|
69
68
|
const text = useSettled(q.trim(), 300);
|
|
70
69
|
const first = useRead(signed ? () => shelf(t, { text, limit: PAGE }) : null, null, [t, signed, text]);
|
|
71
70
|
const [more, setMore] = useState([]);
|
|
@@ -73,7 +72,7 @@ function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
|
73
72
|
const [note, setNote] = useState('');
|
|
74
73
|
useEffect(() => setMore([]), [first.value]);
|
|
75
74
|
if (!signed) {
|
|
76
|
-
return (_jsx(Part, { title: "The shelf", detail: "MCP servers the public registries publish.", children: _jsx(
|
|
75
|
+
return (_jsx(Part, { title: "The shelf", detail: "MCP servers the public registries publish.", children: _jsx(Visitor, { children: "Sign in to browse the shelf." }) }));
|
|
77
76
|
}
|
|
78
77
|
const page = first.value;
|
|
79
78
|
const list = [...(page?.listings ?? []), ...more];
|
|
@@ -88,7 +87,7 @@ function Shelf({ q, signed, may, added, onOpen, onAdd }) {
|
|
|
88
87
|
setMore((m) => [...m, ...got.listings]);
|
|
89
88
|
}
|
|
90
89
|
catch (e) {
|
|
91
|
-
setNote(e
|
|
90
|
+
setNote(e.message);
|
|
92
91
|
}
|
|
93
92
|
finally {
|
|
94
93
|
setPaging(false);
|
|
@@ -153,12 +152,12 @@ function Connect({ listing, onClose, onAdded }) {
|
|
|
153
152
|
said = its.length ? `${s.name} is added, and its ${its.length} tools are on.` : `${s.name} is added. It listed no tools yet.`;
|
|
154
153
|
}
|
|
155
154
|
catch (e) {
|
|
156
|
-
said = `${s.name} is added, but its tools could not be switched on: ${e
|
|
155
|
+
said = `${s.name} is added, but its tools could not be switched on: ${e.message}`;
|
|
157
156
|
}
|
|
158
157
|
onAdded(said);
|
|
159
158
|
}
|
|
160
159
|
catch (e) {
|
|
161
|
-
setNote(e
|
|
160
|
+
setNote(e.message);
|
|
162
161
|
}
|
|
163
162
|
finally {
|
|
164
163
|
setWorking(false);
|
|
@@ -183,7 +182,7 @@ function Detail({ server, may, onClose, onDeleted }) {
|
|
|
183
182
|
list.reload();
|
|
184
183
|
}
|
|
185
184
|
catch (e) {
|
|
186
|
-
setNote(e
|
|
185
|
+
setNote(e.message);
|
|
187
186
|
}
|
|
188
187
|
finally {
|
|
189
188
|
setBusy(false);
|
package/lib/customize/index.js
CHANGED
|
@@ -5,8 +5,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* tab is its own address; in each, Yours is what the org has and Discover is
|
|
6
6
|
* what it can add, with one search box and one Add over both.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Skills and connectors ride into every run in the org, so adding, switching
|
|
9
|
+
* and removing them is an org admin's and a member reads them; plugins and
|
|
10
|
+
* agents are any member's.
|
|
10
11
|
*/
|
|
11
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
13
|
import { BookOpen, Bot, Plug, Plus, Puzzle } from '@hanzogui/lucide-icons-2';
|
package/lib/customize/plugins.js
CHANGED
|
@@ -18,7 +18,7 @@ import { useState } from 'react';
|
|
|
18
18
|
import { authored, build, mounted, remove } from '../api/plugins.js';
|
|
19
19
|
import { useRead } from '../data.js';
|
|
20
20
|
import { useHost, useTarget } from '../host.js';
|
|
21
|
-
import { Choice, Confirm, day, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile } from './ui.js';
|
|
21
|
+
import { Choice, Confirm, day, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile, Visitor } from './ui.js';
|
|
22
22
|
const kb = (n) => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
|
|
23
23
|
export function Plugins({ view, q, adding, onAdding, onView }) {
|
|
24
24
|
const host = useHost();
|
|
@@ -40,7 +40,7 @@ export function Plugins({ view, q, adding, onAdding, onView }) {
|
|
|
40
40
|
mine.reload();
|
|
41
41
|
} })) : null] }));
|
|
42
42
|
if (!signed) {
|
|
43
|
-
return _jsx(
|
|
43
|
+
return _jsx(Visitor, { children: view === 'discover' ? 'Sign in to see what this deployment mounts.' : 'Sign in to see your plugins.' });
|
|
44
44
|
}
|
|
45
45
|
if (view === 'discover') {
|
|
46
46
|
const shown = mounts.value.filter((m) => matches(q, m.name, ...m.prefixes));
|
|
@@ -72,7 +72,7 @@ function Build({ onClose, onBuilt }) {
|
|
|
72
72
|
onBuilt(out.plugin, `${out.plugin.name} built: ${kb(out.bytes)}${out.generated ? ', written from your description — read it below' : ''}.`);
|
|
73
73
|
}
|
|
74
74
|
catch (e) {
|
|
75
|
-
setNote(e
|
|
75
|
+
setNote(e.message);
|
|
76
76
|
}
|
|
77
77
|
finally {
|
|
78
78
|
setWorking(false);
|
package/lib/customize/skills.js
CHANGED
|
@@ -17,7 +17,7 @@ import { active, authored, brand, document, nameOf, remove, tool, write } from '
|
|
|
17
17
|
import { toggle } from '../api/tools.js';
|
|
18
18
|
import { useRead } from '../data.js';
|
|
19
19
|
import { useHost, useTarget } from '../host.js';
|
|
20
|
-
import { Add, Confirm, day, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile } from './ui.js';
|
|
20
|
+
import { Add, Confirm, day, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, Visitor } from './ui.js';
|
|
21
21
|
const PAGE = 60;
|
|
22
22
|
export function Skills({ view, q, adding, onAdding, onView }) {
|
|
23
23
|
const host = useHost();
|
|
@@ -45,7 +45,7 @@ export function Skills({ view, q, adding, onAdding, onView }) {
|
|
|
45
45
|
setNote(next ? `${name} is on` : `${name} is off`);
|
|
46
46
|
}
|
|
47
47
|
catch (e) {
|
|
48
|
-
setNote(e
|
|
48
|
+
setNote(e.message);
|
|
49
49
|
}
|
|
50
50
|
finally {
|
|
51
51
|
setBusy('');
|
|
@@ -74,7 +74,7 @@ export function Skills({ view, q, adding, onAdding, onView }) {
|
|
|
74
74
|
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), shelf.error && !shelf.value ? (_jsx(Soft, { children: shelf.error.message })) : !shelf.value ? (_jsx(Soft, { children: "Reading the catalogue\u2026" })) : !found.length ? (_jsx(Soft, { children: all.length ? 'No skill matches.' : 'The catalogue lists no skills.' })) : (_jsxs(_Fragment, { children: [_jsx(SizableText, { size: "$1", color: "$soft", children: found.length === all.length ? `${all.length} skills across ${shelf.value.products.length} products` : `${found.length} of ${all.length} skills` }), _jsx(Grid, { label: "Skills to add", children: found.slice(0, shown).map((e) => (_jsx(Tile, { title: e.name, detail: e.description, meta: e.product, mark: _jsx(Mark, { name: e.name, icon: _jsx(BookOpen, { size: 15 }) }), onOpen: () => setReading(e), action: may ? _jsx(Add, { name: e.name, added: lit.has(tool(e.name)), busy: busy === e.name, onPress: () => void flip(e.name, true) }) : undefined }, e.name))) }), found.length > shown ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setShown(shown + PAGE), children: "Show more" }) })) : null] })), reader, editor] }));
|
|
75
75
|
}
|
|
76
76
|
if (!signed) {
|
|
77
|
-
return _jsx(
|
|
77
|
+
return _jsx(Visitor, { children: "Sign in to see your skills." });
|
|
78
78
|
}
|
|
79
79
|
const ownShown = own.value.filter((s) => matches(q, s.name, s.description));
|
|
80
80
|
const added = on.value.filter((x) => nameOf(x.name) && !mine.has(nameOf(x.name)) && matches(q, nameOf(x.name), x.description));
|
|
@@ -114,7 +114,7 @@ function Editor({ skill, may, onClose, onSaved, onDeleted, }) {
|
|
|
114
114
|
onSaved(saved, !skill);
|
|
115
115
|
}
|
|
116
116
|
catch (e) {
|
|
117
|
-
setNote(e
|
|
117
|
+
setNote(e.message);
|
|
118
118
|
}
|
|
119
119
|
finally {
|
|
120
120
|
setWorking(false);
|
package/lib/customize/ui.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function Tile({ title, detail, meta, mark, action, onOpen, }: {
|
|
|
47
47
|
export declare function Featured({ title, detail, meta, mark, action, onOpen }: {
|
|
48
48
|
title: string;
|
|
49
49
|
detail: string;
|
|
50
|
-
meta
|
|
50
|
+
meta: string;
|
|
51
51
|
mark: ReactNode;
|
|
52
52
|
action?: ReactNode;
|
|
53
53
|
onOpen: () => void;
|
|
@@ -80,6 +80,10 @@ export declare function Soft({ children, action }: {
|
|
|
80
80
|
children: string;
|
|
81
81
|
action?: ReactNode;
|
|
82
82
|
}): import("react").JSX.Element;
|
|
83
|
+
/** What a signed-out visitor is told, with the one control that signs them in. */
|
|
84
|
+
export declare function Visitor({ children }: {
|
|
85
|
+
children: string;
|
|
86
|
+
}): import("react").JSX.Element;
|
|
83
87
|
/** What the last action did, said once, where it was done. */
|
|
84
88
|
export declare function Line({ children }: {
|
|
85
89
|
children: string;
|
|
@@ -87,7 +91,7 @@ export declare function Line({ children }: {
|
|
|
87
91
|
/** A small heading over a run of cards. */
|
|
88
92
|
export declare function Part({ title, detail, children }: {
|
|
89
93
|
title: string;
|
|
90
|
-
detail
|
|
94
|
+
detail: string;
|
|
91
95
|
children: ReactNode;
|
|
92
96
|
}): import("react").JSX.Element;
|
|
93
97
|
/** A labelled control, with a line under it when there is more to say. */
|