@hanzo/build 0.2.16 → 0.2.17
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/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/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/git.js +1 -1
- package/lib/run.js +72 -45
- package/lib/transcript.d.ts +5 -2
- package/lib/transcript.js +21 -3
- 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/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. */
|
package/lib/customize/ui.js
CHANGED
|
@@ -9,6 +9,7 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
|
9
9
|
import { Check, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
10
10
|
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
11
11
|
import { useState } from 'react';
|
|
12
|
+
import { useHost } from '../host.js';
|
|
12
13
|
export const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
13
14
|
/** A button that holds a card's text reads from the left, not from the middle a button centres it in. */
|
|
14
15
|
export const left = { textAlign: 'left' };
|
|
@@ -40,7 +41,7 @@ export function Tile({ title, detail, meta, mark, action, onOpen, }) {
|
|
|
40
41
|
}
|
|
41
42
|
/** The one card the shelf puts first, drawn larger. */
|
|
42
43
|
export function Featured({ title, detail, meta, mark, action, onOpen }) {
|
|
43
|
-
return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", style: left, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: "Featured" }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }),
|
|
44
|
+
return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", style: left, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: "Featured" }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta })] })] }), action] }));
|
|
44
45
|
}
|
|
45
46
|
/** A card's add control: a plus while it is not yours, a check once it is. */
|
|
46
47
|
export function Add({ name, added, busy, onPress }) {
|
|
@@ -65,7 +66,7 @@ export function Confirm({ what, says, open, onOpenChange, onYes }) {
|
|
|
65
66
|
onOpenChange(false);
|
|
66
67
|
}
|
|
67
68
|
catch (e) {
|
|
68
|
-
setNote(e
|
|
69
|
+
setNote(e.message);
|
|
69
70
|
}
|
|
70
71
|
finally {
|
|
71
72
|
setBusy(false);
|
|
@@ -77,6 +78,11 @@ export function Confirm({ what, says, open, onOpenChange, onYes }) {
|
|
|
77
78
|
export function Soft({ children, action }) {
|
|
78
79
|
return (_jsxs(YStack, { py: "$6", items: "center", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }), action] }));
|
|
79
80
|
}
|
|
81
|
+
/** What a signed-out visitor is told, with the one control that signs them in. */
|
|
82
|
+
export function Visitor({ children }) {
|
|
83
|
+
const host = useHost();
|
|
84
|
+
return (_jsx(Soft, { action: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }), children: children }));
|
|
85
|
+
}
|
|
80
86
|
/** What the last action did, said once, where it was done. */
|
|
81
87
|
export function Line({ children }) {
|
|
82
88
|
if (!children)
|
|
@@ -85,7 +91,7 @@ export function Line({ children }) {
|
|
|
85
91
|
}
|
|
86
92
|
/** A small heading over a run of cards. */
|
|
87
93
|
export function Part({ title, detail, children }) {
|
|
88
|
-
return (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }),
|
|
94
|
+
return (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: "$soft", children: detail })] }), children] }));
|
|
89
95
|
}
|
|
90
96
|
/** A labelled control, with a line under it when there is more to say. */
|
|
91
97
|
export function Field({ label, hint, children }) {
|
package/lib/desk.d.ts
CHANGED
|
@@ -17,11 +17,11 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ti
|
|
|
17
17
|
sandbox: string;
|
|
18
18
|
events: Event[];
|
|
19
19
|
live: boolean;
|
|
20
|
-
/** What can be done to the run — rename, share, copy its id — as its header
|
|
21
|
-
menu
|
|
20
|
+
/** What can be done to the run — rename, share, copy its id — as its header offers it. */
|
|
21
|
+
menu: NonNullable<DropdownMenuProps['items']>;
|
|
22
22
|
refused: string;
|
|
23
23
|
/** The refusal's one action: Retry, or Sign in when nobody is. */
|
|
24
|
-
retry
|
|
24
|
+
retry: string;
|
|
25
25
|
onRetry: () => void;
|
|
26
|
-
onHide
|
|
26
|
+
onHide: () => void;
|
|
27
27
|
}): import("react").JSX.Element;
|
package/lib/desk.js
CHANGED
|
@@ -14,13 +14,15 @@ import { Files } from './files.js';
|
|
|
14
14
|
import { Git } from './git.js';
|
|
15
15
|
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
16
16
|
const BLEED = ['desktop', 'terminal'];
|
|
17
|
-
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu
|
|
18
|
-
//
|
|
19
|
-
|
|
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.
|
|
20
|
+
const [picked, setTab] = useState(null);
|
|
21
|
+
const tab = picked ?? (mode === 'setup' ? 'environment' : 'terminal');
|
|
20
22
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
21
23
|
const lines = shell(events);
|
|
22
24
|
const bleed = BLEED.includes(tab);
|
|
23
|
-
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" })] }),
|
|
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] })] }));
|
|
24
26
|
}
|
|
25
27
|
function TabButton({ id, tab, onPick, children }) {
|
|
26
28
|
const on = tab === id;
|
package/lib/door.js
CHANGED
package/lib/environment.js
CHANGED
|
@@ -63,7 +63,7 @@ export function Environment({ repo, busy }) {
|
|
|
63
63
|
return true;
|
|
64
64
|
}
|
|
65
65
|
catch (e) {
|
|
66
|
-
setNote(e
|
|
66
|
+
setNote(e.message);
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
69
|
finally {
|
|
@@ -78,7 +78,7 @@ export function Environment({ repo, busy }) {
|
|
|
78
78
|
host.go(run.session);
|
|
79
79
|
}
|
|
80
80
|
catch (e) {
|
|
81
|
-
setNote(e
|
|
81
|
+
setNote(e.message);
|
|
82
82
|
setWorking(false);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
@@ -116,7 +116,7 @@ export function SetupDialog({ repo, open, onOpenChange, onStart, onSaved, }) {
|
|
|
116
116
|
done();
|
|
117
117
|
}
|
|
118
118
|
catch (e) {
|
|
119
|
-
setNote(e
|
|
119
|
+
setNote(e.message);
|
|
120
120
|
}
|
|
121
121
|
finally {
|
|
122
122
|
setWorking(false);
|
package/lib/git.js
CHANGED
|
@@ -39,7 +39,7 @@ export function Git({ session, title, live }) {
|
|
|
39
39
|
: 'Open'
|
|
40
40
|
: c.pull.state === 'merged'
|
|
41
41
|
? 'Merged'
|
|
42
|
-
: 'Closed' }), c.pull.url.startsWith('https://') ? (_jsx(Out, { href: c.pull.url, label: `Open pull request #${c.pull.number}`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Open on the forge" }) })) : null] }), c.pull.reviews.length ? (c.pull.reviews.map((r, i) => (_jsxs(YStack, { gap: "$1", pb: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children:
|
|
42
|
+
: 'Closed' }), c.pull.url.startsWith('https://') ? (_jsx(Out, { href: c.pull.url, label: `Open pull request #${c.pull.number}`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Open on the forge" }) })) : null] }), c.pull.reviews.length ? (c.pull.reviews.map((r, i) => (_jsxs(YStack, { gap: "$1", pb: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: [r.author || 'Someone', verdict(r.state), when(r.at)].filter(Boolean).join(' · ') }), r.body ? (_jsx(SizableText, { size: "$2", color: "$ink", children: r.body })) : null] }, i)))) : (_jsx(Soft, { children: "No reviews yet" }))] })) : (_jsx(Soft, { children: "No pull request yet. The run opens one when it pushes its changes." }))) : c.commits.length ? (_jsx(YStack, { children: c.commits.map((k) => (_jsxs(XStack, { gap: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: k.sha.slice(0, 7) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 2, children: k.message }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [k.author, when(k.date)].filter(Boolean).join(' · ') })] })] }, k.sha))) })) : (_jsx(Soft, { children: "No pushed commits" }))] }));
|
|
43
43
|
}
|
|
44
44
|
function File({ change: f, first }) {
|
|
45
45
|
const [open, setOpen] = useState(false);
|
package/lib/run.js
CHANGED
|
@@ -15,23 +15,36 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
* A sandbox run is one agent invocation that takes its task on its command line
|
|
16
16
|
* (apps/coding steer.go), so what continues it is a new run: a paused run is
|
|
17
17
|
* carried on, and a finished one followed up, by `POST /v1/agent/coding` from
|
|
18
|
-
* the branch its work was kept on — the platform's own continue, done here
|
|
18
|
+
* the branch its work was kept on — the platform's own continue, done here —
|
|
19
|
+
* with the model and effort the composer's foot shows, which New shares.
|
|
20
|
+
*
|
|
21
|
+
* It reads as a chat: the person's ask opens it, each thing the agent said can
|
|
22
|
+
* be copied and judged (a verdict is `build.verdict` on the event bus, as a
|
|
23
|
+
* project's turns send it), and the title is the menu of what can be done to
|
|
24
|
+
* the run. Share opens its story to the public build route.
|
|
19
25
|
*/
|
|
20
26
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
21
|
-
import { Copy, ExternalLink, GitPullRequest, Globe,
|
|
27
|
+
import { ChevronDown, ChevronRight, Copy, ExternalLink, GitPullRequest, Globe, PanelRight, Pencil, Share } from '@hanzogui/lucide-icons-2';
|
|
22
28
|
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
23
29
|
import { Steer } from '@hanzo/ui/agents';
|
|
24
30
|
import { Composer } from '@hanzo/ui/chat';
|
|
31
|
+
import { ChipSelect } from '@hanzo/ui/product';
|
|
25
32
|
import { useEffect, useMemo, useState } from 'react';
|
|
26
33
|
import { approve, followUp, start } from './api/coding.js';
|
|
34
|
+
import { ENSO, label as named, models } from './api/models.js';
|
|
27
35
|
import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
|
|
28
36
|
import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
|
|
37
|
+
import { verdict } from './api/verdict.js';
|
|
29
38
|
import { useKept, useRead, useRun } from './data.js';
|
|
30
39
|
import { Desk } from './desk.js';
|
|
31
40
|
import { useHost, useTarget } from './host.js';
|
|
41
|
+
import { EFFORTS } from './landing.js';
|
|
32
42
|
import { Out } from './out.js';
|
|
43
|
+
import { usePrefs } from './prefs.js';
|
|
33
44
|
import { Transcript } from './transcript.js';
|
|
34
45
|
const LIVE = new Set(['running', 'paused', '']);
|
|
46
|
+
// What this browser kept is read as it is found: a stored value that is not a string is none.
|
|
47
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
35
48
|
export function Run({ id }) {
|
|
36
49
|
const host = useHost();
|
|
37
50
|
const t = useTarget();
|
|
@@ -48,6 +61,13 @@ export function Run({ id }) {
|
|
|
48
61
|
const [naming, setNaming] = useState(false);
|
|
49
62
|
const [name, setName] = useState('');
|
|
50
63
|
const [copied, setCopied] = useState('');
|
|
64
|
+
const [unfolded, setUnfolded] = useState(false);
|
|
65
|
+
// The model and effort a run started here uses: New's choice, which a change here changes too.
|
|
66
|
+
const { prefs } = usePrefs();
|
|
67
|
+
const [chose, choose] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
|
|
68
|
+
const model = text(chose?.model) || prefs.code?.model || ENSO;
|
|
69
|
+
const pace = EFFORTS.find((e) => e.id === chose?.effort) ?? EFFORTS.find((e) => e.id === prefs.code?.effort) ?? EFFORTS[1];
|
|
70
|
+
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
51
71
|
const mode = record?.mode ?? '';
|
|
52
72
|
const shown = useMemo(() => cards(events, mode), [events, mode]);
|
|
53
73
|
const end = outcome(events);
|
|
@@ -62,9 +82,11 @@ export function Run({ id }) {
|
|
|
62
82
|
// A sandbox run is held by the coding service, which pauses it with its work
|
|
63
83
|
// kept and has no resume of its own; a machine drains its commands itself.
|
|
64
84
|
const held = (record?.environment || 'sandbox') === 'sandbox';
|
|
85
|
+
// Words go to the run while it works, unless it is a paused sandbox run, which a new run carries on.
|
|
86
|
+
const steering = running && !(paused && held);
|
|
65
87
|
// A follow-up clones what the run kept, so it waits until the run has said where that is.
|
|
66
88
|
const waiting = (paused && held && !kept.settled) || (finished && stopping && !kept.settled);
|
|
67
|
-
const title = record?.title ||
|
|
89
|
+
const title = record?.title || (detail.loading ? '' : 'Untitled run');
|
|
68
90
|
// A setup run explores and installs before it answers, which takes minutes.
|
|
69
91
|
const setup = mode === 'setup';
|
|
70
92
|
const [seen, setSeen] = useKept('hanzo.build.setup.seen', false);
|
|
@@ -86,15 +108,17 @@ export function Run({ id }) {
|
|
|
86
108
|
const planned = mode === 'plan' && finished && state === 'done' ? answer(events) : '';
|
|
87
109
|
// What the recorded read left out: it carries the latest fifty turns.
|
|
88
110
|
const hidden = Math.max(0, (detail.value?.events ?? 0) - (detail.value?.recent.length ?? 0));
|
|
111
|
+
// Only a run still working is on a step.
|
|
112
|
+
const current = running ? plan.find((x) => !x.done) : undefined;
|
|
113
|
+
// Asked for while it ran, said once when it stops. `notify` is only ever set
|
|
114
|
+
// where Notification exists; the permission can be taken back since.
|
|
89
115
|
useEffect(() => {
|
|
90
116
|
if (!notify || running)
|
|
91
117
|
return;
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
const n = new Notification(title || 'Run finished', { body: 'This run has finished.' });
|
|
118
|
+
if (Notification.permission === 'granted')
|
|
119
|
+
new Notification(record?.title || 'Run finished', { body: 'This run has finished.' });
|
|
95
120
|
setNotify(false);
|
|
96
|
-
|
|
97
|
-
}, [notify, running, title]);
|
|
121
|
+
}, [notify, running, record?.title]);
|
|
98
122
|
const ask = async () => {
|
|
99
123
|
if (typeof Notification === 'undefined') {
|
|
100
124
|
setNote('This browser cannot send a notification.');
|
|
@@ -108,8 +132,8 @@ export function Run({ id }) {
|
|
|
108
132
|
setNotify(true);
|
|
109
133
|
setNote('');
|
|
110
134
|
};
|
|
111
|
-
/** Something that answers a sentence for the note, or throws the reason. */
|
|
112
|
-
const act = async (work
|
|
135
|
+
/** Something that answers a sentence for the note, or throws the reason. One at a time. */
|
|
136
|
+
const act = async (work) => {
|
|
113
137
|
if (busy)
|
|
114
138
|
return;
|
|
115
139
|
setBusy(true);
|
|
@@ -118,36 +142,37 @@ export function Run({ id }) {
|
|
|
118
142
|
setNote(await work());
|
|
119
143
|
}
|
|
120
144
|
catch (e) {
|
|
121
|
-
setNote(e
|
|
145
|
+
setNote(e.message);
|
|
122
146
|
}
|
|
123
147
|
finally {
|
|
124
148
|
setBusy(false);
|
|
125
149
|
}
|
|
126
150
|
};
|
|
151
|
+
/** A new run's ask with the model and effort the foot shows. The router is the platform's default, so it is not named. */
|
|
152
|
+
const tuned = (a) => ({ ...a, model: model === ENSO ? undefined : model, effort: pace.id });
|
|
127
153
|
/** A new run from this one's work, opened. A paused run it carries on is let go once the new one is admitted. */
|
|
128
154
|
const carry = async (said, how = 'follow') => {
|
|
129
155
|
if (!earlier)
|
|
130
156
|
throw new Error('This run is still being read');
|
|
131
|
-
const run = await start(t, how === 'approve' ? approve(earlier, said) : followUp(earlier, said));
|
|
157
|
+
const run = await start(t, tuned(how === 'approve' ? approve(earlier, said) : followUp(earlier, said)));
|
|
132
158
|
if (paused)
|
|
133
159
|
await stop(t, id, 'Continued in a follow-up run').catch(() => undefined);
|
|
134
160
|
setDraft('');
|
|
135
161
|
host.go(run.session);
|
|
136
162
|
return '';
|
|
137
163
|
};
|
|
164
|
+
// The composer sends only words, and only while it is enabled.
|
|
138
165
|
const send = () => {
|
|
139
166
|
const words = draft.trim();
|
|
140
|
-
if (
|
|
141
|
-
return;
|
|
142
|
-
if (running && !(paused && held)) {
|
|
167
|
+
if (steering) {
|
|
143
168
|
void act(async () => {
|
|
144
169
|
await message(t, id, words);
|
|
145
170
|
setDraft('');
|
|
146
171
|
return 'Sent — recorded on this run';
|
|
147
|
-
}
|
|
172
|
+
});
|
|
148
173
|
return;
|
|
149
174
|
}
|
|
150
|
-
void act(() => carry(words)
|
|
175
|
+
void act(() => carry(words));
|
|
151
176
|
};
|
|
152
177
|
const command = (c) => {
|
|
153
178
|
switch (c) {
|
|
@@ -156,22 +181,22 @@ export function Run({ id }) {
|
|
|
156
181
|
await stop(t, id);
|
|
157
182
|
setStopping(true);
|
|
158
183
|
return 'Stop requested — the run keeps its work on its branch';
|
|
159
|
-
}
|
|
184
|
+
});
|
|
160
185
|
return;
|
|
161
186
|
case 'pause':
|
|
162
187
|
void act(async () => {
|
|
163
188
|
await pause(t, id);
|
|
164
189
|
return held ? 'Pause requested — the run keeps its work on its branch and waits' : 'Pause asked — the machine pauses when it reads it';
|
|
165
|
-
}
|
|
190
|
+
});
|
|
166
191
|
return;
|
|
167
192
|
case 'resume':
|
|
168
193
|
if (held)
|
|
169
|
-
void act(() => carry('')
|
|
194
|
+
void act(() => carry(''));
|
|
170
195
|
else
|
|
171
196
|
void act(async () => {
|
|
172
197
|
await resume(t, id);
|
|
173
198
|
return 'Resume asked — the machine goes on when it reads it';
|
|
174
|
-
}
|
|
199
|
+
});
|
|
175
200
|
return;
|
|
176
201
|
}
|
|
177
202
|
};
|
|
@@ -188,9 +213,9 @@ export function Run({ id }) {
|
|
|
188
213
|
await publish(t, id, on);
|
|
189
214
|
detail.reload();
|
|
190
215
|
return on ? 'Shared — anyone with the link can read this run’s story' : 'No longer shared';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const
|
|
216
|
+
});
|
|
217
|
+
// What the title's menu does to the run.
|
|
218
|
+
const manage = [
|
|
194
219
|
...(signed && record
|
|
195
220
|
? [
|
|
196
221
|
{
|
|
@@ -204,25 +229,24 @@ export function Run({ id }) {
|
|
|
204
229
|
},
|
|
205
230
|
]
|
|
206
231
|
: []),
|
|
207
|
-
// Sharing opens the public build route, which is addressed by the project a run built.
|
|
208
|
-
...(signed && record?.project
|
|
209
|
-
? [
|
|
210
|
-
record.published
|
|
211
|
-
? { key: 'unshare', label: 'Stop sharing', icon: _jsx(Globe, { size: 16 }), onSelect: () => void share(false) }
|
|
212
|
-
: { key: 'share', label: 'Share publicly', icon: _jsx(Globe, { size: 16 }), description: 'Anyone with the link can read it', onSelect: () => void share(true) },
|
|
213
|
-
]
|
|
214
|
-
: []),
|
|
215
|
-
...(record?.published && link
|
|
216
|
-
? [{ key: 'link', label: copied === 'link' ? 'Copied' : 'Copy public link', icon: _jsx(Copy, { size: 16 }), description: link, onSelect: () => void copy('link', link) }]
|
|
217
|
-
: []),
|
|
218
232
|
{ key: 'copy', label: copied === 'id' ? 'Copied' : 'Copy run id', icon: _jsx(Copy, { size: 16 }), description: id, onSelect: () => void copy('id', id) },
|
|
219
233
|
];
|
|
234
|
+
// Sharing opens the public build route, which is addressed by the org and the project a run built.
|
|
235
|
+
const link = record?.published ? story(t, record.org || host.org, record.project) : '';
|
|
236
|
+
const sharing = signed && record?.project
|
|
237
|
+
? [
|
|
238
|
+
record.published
|
|
239
|
+
? { key: 'unshare', label: 'Stop sharing', icon: _jsx(Globe, { size: 16 }), onSelect: () => void share(false) }
|
|
240
|
+
: { key: 'share', label: 'Share publicly', icon: _jsx(Globe, { size: 16 }), description: 'Anyone with the link can read it', onSelect: () => void share(true) },
|
|
241
|
+
...(link ? [{ key: 'link', label: copied === 'link' ? 'Copied' : 'Copy public link', icon: _jsx(Copy, { size: 16 }), description: link, onSelect: () => void copy('link', link) }] : []),
|
|
242
|
+
]
|
|
243
|
+
: [];
|
|
220
244
|
const save = () => void act(async () => {
|
|
221
245
|
await rename(t, id, name);
|
|
222
246
|
setNaming(false);
|
|
223
247
|
detail.reload();
|
|
224
248
|
return 'Renamed';
|
|
225
|
-
}
|
|
249
|
+
});
|
|
226
250
|
const placeholder = !signed
|
|
227
251
|
? 'Sign in to follow up'
|
|
228
252
|
: waiting
|
|
@@ -238,9 +262,11 @@ export function Run({ id }) {
|
|
|
238
262
|
: finished
|
|
239
263
|
? 'Follow up — continues in a new run'
|
|
240
264
|
: 'Reading this run…';
|
|
241
|
-
|
|
265
|
+
// What a new run from here is made with; words that steer this run use neither.
|
|
266
|
+
const tuning = signed && !steering ? (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", justify: "flex-end", flexWrap: "wrap", rowGap: "$1", children: [_jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === model)?.label ?? named(model), chosen: catalog.value.find((m) => m.id === model) ?? null, items: catalog.value, onChange: (m) => choose({ ...chose, model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: pace.label, chosen: pace, items: EFFORTS, onChange: (e) => choose({ ...chose, effort: e.id }), placement: "top-end", width: 180 })] })) : undefined;
|
|
267
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
|
|
242
268
|
// A paused run's agent was stopped where it stood; what it was running is cut off.
|
|
243
|
-
live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve')
|
|
269
|
+
live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve')) : undefined, approving: busy, onVerdict: (v) => verdict(t, id, record?.project ?? '', v), empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
|
|
244
270
|
? detail.error.message
|
|
245
271
|
: detail.loading
|
|
246
272
|
? 'Reading this run…'
|
|
@@ -248,11 +274,12 @@ export function Run({ id }) {
|
|
|
248
274
|
? setup
|
|
249
275
|
? 'Setting up environment'
|
|
250
276
|
: 'Starting…'
|
|
251
|
-
: 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
277
|
+
: 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$3", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || (signed && refused) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-label": `Steps · ${plan.length}`, "aria-expanded": unfolded, onPress: () => setUnfolded(!unfolded), px: "$3", py: "$1.5", gap: "$2", items: "center", hoverStyle: { bg: '$hover' }, children: [unfolded ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: `Steps · ${plan.length}` }), current && !unfolded ? (_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, style: { textAlign: 'right' }, children: current.name })) : null] }), unfolded
|
|
278
|
+
? plan.map((s) => {
|
|
279
|
+
const on = s === current;
|
|
280
|
+
return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || on ? '$ink' : '$soft', children: s.done ? '✓' : on ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
|
|
281
|
+
})
|
|
282
|
+
: null] })) : null, setup && running && !seen ? _jsx(Onboarding, { onDone: () => setSeen(true) }) : null, running ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 160, children: paused
|
|
256
283
|
? kept.pushed
|
|
257
284
|
? 'Paused — its work so far is on its branch.'
|
|
258
285
|
: 'Paused.'
|
|
@@ -263,8 +290,8 @@ export function Run({ id }) {
|
|
|
263
290
|
: 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
|
|
264
291
|
...(paused ? ['pause'] : ['resume']),
|
|
265
292
|
...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
|
|
266
|
-
] }), _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:
|
|
267
|
-
if (
|
|
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) => {
|
|
294
|
+
if (e.key === 'Enter')
|
|
268
295
|
save();
|
|
269
296
|
} }), _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" })] })] }) })] }));
|
|
270
297
|
}
|
package/lib/transcript.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Verdict } from '@hanzo/ui/agents';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
2
3
|
import type { Card } from './api/turn.ts';
|
|
3
|
-
export declare function Transcript({ cards, live, empty, header, onApprove, approving, }: {
|
|
4
|
+
export declare function Transcript({ cards, live, empty, header, onApprove, approving, onVerdict, }: {
|
|
4
5
|
cards: Card[];
|
|
5
6
|
/** The run is still working: a card still running shows it; otherwise it was cut off. */
|
|
6
7
|
live: boolean;
|
|
@@ -9,4 +10,6 @@ export declare function Transcript({ cards, live, empty, header, onApprove, appr
|
|
|
9
10
|
/** Build what a plan says. Absent, a plan has no button. */
|
|
10
11
|
onApprove?: (plan: string) => void;
|
|
11
12
|
approving?: boolean;
|
|
13
|
+
/** Record a verdict on what the agent said; throws when it did not land. */
|
|
14
|
+
onVerdict: (v: Verdict) => Promise<void>;
|
|
12
15
|
}): import("react").JSX.Element;
|
package/lib/transcript.js
CHANGED
|
@@ -4,6 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* agent said as markdown, each command it ran as a card that opens onto its
|
|
5
5
|
* output, the files it changed with their diff, the files it read as chips,
|
|
6
6
|
* the run's own steps, and a plan run's plan with the button that builds it.
|
|
7
|
+
* What the agent said, and its plan, can be copied and judged, as a reply on
|
|
8
|
+
* claude.ai can: a thumb pressed again takes the verdict back.
|
|
7
9
|
*
|
|
8
10
|
* It follows the run to the bottom as it streams and stops when the reader
|
|
9
11
|
* scrolls up (Thread). Everything drawn is text.
|
|
@@ -11,7 +13,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
14
|
import { FileText } from '@hanzogui/lucide-icons-2';
|
|
13
15
|
import { Button } from '@hanzo/ui';
|
|
16
|
+
import { Feedback } from '@hanzo/ui/agents';
|
|
14
17
|
import { Code, Message, Step, Thread } from '@hanzo/ui/chat';
|
|
18
|
+
import { useState } from 'react';
|
|
15
19
|
import { Patch } from './git.js';
|
|
16
20
|
import { Prose } from './prose.js';
|
|
17
21
|
/** A card's output, its tail: what a command said last is why it stopped. */
|
|
@@ -30,7 +34,7 @@ function rows(cards) {
|
|
|
30
34
|
}
|
|
31
35
|
return out;
|
|
32
36
|
}
|
|
33
|
-
export function Transcript({ cards, live, empty, header, onApprove, approving = false, }) {
|
|
37
|
+
export function Transcript({ cards, live, empty, header, onApprove, approving = false, onVerdict, }) {
|
|
34
38
|
// A card the run never finished is cut off once the run has ended, not still going.
|
|
35
39
|
const state = (ran) => (ran === 'running' && !live ? 'cancelled' : ran);
|
|
36
40
|
return (_jsxs(Thread, { maxWidth: 0, gap: 14, column: { px: 0 }, "aria-label": "Transcript", children: [header, cards.length === 0 ? _jsx(YStack, { py: "$2", children: empty }) : null, rows(cards).map((row) => {
|
|
@@ -40,7 +44,7 @@ export function Transcript({ cards, live, empty, header, onApprove, approving =
|
|
|
40
44
|
const c = row;
|
|
41
45
|
switch (c.kind) {
|
|
42
46
|
case 'said':
|
|
43
|
-
return c.who === 'person' ? (_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$3", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: c.text }) }, c.key)) : (_jsx(Message, { role: "assistant", children: _jsx(Prose, { text: c.text }) }, c.key));
|
|
47
|
+
return c.who === 'person' ? (_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$3", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: c.text }) }, c.key)) : (_jsx(Message, { role: "assistant", actions: _jsx(Judge, { text: c.text, onVerdict: onVerdict }), children: _jsx(Prose, { text: c.text }) }, c.key));
|
|
44
48
|
case 'shell':
|
|
45
49
|
return (_jsx(Step, { name: "Shell", detail: c.command, status: state(c.ran), "aria-label": `Command ${c.command}`, children: _jsx(Code, { language: "shell", value: c.output ? `$ ${c.command}\n${c.output}` : `$ ${c.command}`, children: c.output ? `$ ${c.command}\n${tail(c.output)}` : `$ ${c.command}` }) }, c.key));
|
|
46
50
|
case 'edit':
|
|
@@ -50,7 +54,21 @@ export function Transcript({ cards, live, empty, header, onApprove, approving =
|
|
|
50
54
|
case 'note':
|
|
51
55
|
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: c.text }, c.key));
|
|
52
56
|
case 'plan':
|
|
53
|
-
return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }),
|
|
57
|
+
return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }), _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [onApprove ? (_jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' })) : null, _jsx(Judge, { text: c.text, onVerdict: onVerdict })] })] }, c.key));
|
|
54
58
|
}
|
|
55
59
|
})] }));
|
|
56
60
|
}
|
|
61
|
+
/** Copy what the agent said, and say whether it was good. A verdict that did not land is taken back and says why. */
|
|
62
|
+
function Judge({ text, onVerdict }) {
|
|
63
|
+
const [verdict, setVerdict] = useState(null);
|
|
64
|
+
const [note, setNote] = useState('');
|
|
65
|
+
const judge = (next) => {
|
|
66
|
+
setVerdict(next);
|
|
67
|
+
setNote('');
|
|
68
|
+
onVerdict(next).catch((e) => {
|
|
69
|
+
setVerdict(null);
|
|
70
|
+
setNote(e.message);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
return (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Feedback, { text: text, verdict: verdict, onVerdict: judge }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }));
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
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 page (hanzo.build) and as a component (hanzo.ai/dev).",
|