@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.
Files changed (59) hide show
  1. package/lib/api/codebases.d.ts +1 -1
  2. package/lib/api/codebases.js +3 -3
  3. package/lib/api/harness.d.ts +7 -0
  4. package/lib/api/harness.js +34 -6
  5. package/lib/api/sessions.d.ts +2 -2
  6. package/lib/api/sessions.js +1 -1
  7. package/lib/api/turn.js +37 -16
  8. package/lib/ask.js +2 -2
  9. package/lib/builder.js +36 -4
  10. package/lib/choice.js +2 -2
  11. package/lib/customize/agents.js +4 -4
  12. package/lib/customize/connectors.js +7 -8
  13. package/lib/customize/index.js +3 -2
  14. package/lib/customize/plugins.js +3 -3
  15. package/lib/customize/skills.js +4 -4
  16. package/lib/customize/ui.d.ts +6 -2
  17. package/lib/customize/ui.js +9 -3
  18. package/lib/data.js +17 -15
  19. package/lib/desk.d.ts +4 -4
  20. package/lib/desk.js +6 -4
  21. package/lib/door.js +1 -1
  22. package/lib/environment.js +3 -3
  23. package/lib/find.js +2 -2
  24. package/lib/forge.js +6 -14
  25. package/lib/git.js +1 -1
  26. package/lib/host.d.ts +2 -0
  27. package/lib/landing.js +25 -16
  28. package/lib/markdown.js +6 -4
  29. package/lib/pitch.d.ts +4 -0
  30. package/lib/pitch.js +39 -0
  31. package/lib/plans.js +10 -9
  32. package/lib/project.js +20 -21
  33. package/lib/prose.js +1 -1
  34. package/lib/publish.js +9 -8
  35. package/lib/run.js +72 -45
  36. package/lib/settings/account.js +7 -5
  37. package/lib/settings/billing.js +11 -20
  38. package/lib/settings/capabilities.js +1 -1
  39. package/lib/settings/card.js +2 -2
  40. package/lib/settings/code.js +6 -3
  41. package/lib/settings/general.js +6 -5
  42. package/lib/settings/index.js +2 -3
  43. package/lib/settings/integrations.js +2 -2
  44. package/lib/settings/keys.js +8 -9
  45. package/lib/settings/machines.js +5 -9
  46. package/lib/settings/members.js +3 -2
  47. package/lib/settings/memory.js +1 -1
  48. package/lib/settings/notifications.js +1 -1
  49. package/lib/settings/privacy.js +2 -2
  50. package/lib/settings/usage.js +8 -9
  51. package/lib/shelf.js +5 -9
  52. package/lib/sync.js +2 -5
  53. package/lib/tools.js +2 -3
  54. package/lib/transcript.d.ts +5 -2
  55. package/lib/transcript.js +21 -3
  56. package/lib/visit.d.ts +31 -0
  57. package/lib/visit.js +53 -0
  58. package/lib/voice.js +3 -5
  59. package/package.json +1 -1
@@ -41,7 +41,7 @@ export declare function chosen(r: {
41
41
  full_name?: string;
42
42
  private?: boolean;
43
43
  default_branch?: string;
44
- }, clone?: string): ForgeRepo;
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. */
@@ -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, clone = '') {
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 || 'main',
54
+ default_branch: c.branch,
55
55
  pushed_at: c.updated,
56
56
  installation_id: 0,
57
57
  forge: true,
@@ -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`. */
@@ -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
- if (card.kind === 'step')
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())
@@ -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;
@@ -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
- const parsed = JSON.parse(text);
34
- if (parsed && typeof parsed === 'object')
35
- return parsed;
34
+ return JSON.parse(text);
36
35
  }
37
36
  catch {
38
- /* prose that opens with a brace */
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
- out.push({ kind: 'said', key: mint(), who: 'person', text: said });
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
- out.push({ kind: 'said', key: mint(), who: str(b?.role) === 'user' ? 'person' : 'agent', text });
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
- return out;
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 instanceof Error ? e.message : 'That did not work');
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 ((e.key ?? e.nativeEvent?.key) === 'Enter')
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 = (_jsx(XStack, { render: "button", "aria-label": "Hanzo Build", items: "center", onPress: () => go(''), children: _jsx(SizableText, { size: "$5", fontWeight: "600", color: "$ink", numberOfLines: 1, children: "Hanzo Build" }) }));
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.trim().charAt(0).toUpperCase() || 'H';
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
- return rail ? _jsx(Shell, {}) : _jsx(Pane, {});
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 || 'main', ask });
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 Event('hanzo-board'));
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'}`;
@@ -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+$/, '') || '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(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your agents." });
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 instanceof Error ? e.message : 'The agent was not saved');
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(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your connectors." });
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(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to browse the shelf." }) }));
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 instanceof Error ? e.message : 'The next page did not load');
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 instanceof Error ? e.message : 'no answer'}`;
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 instanceof Error ? e.message : 'The connector was not added');
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 instanceof Error ? e.message : 'That did not work');
185
+ setNote(e.message);
187
186
  }
188
187
  finally {
189
188
  setBusy(false);
@@ -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
- * Nothing here is gated to org admins, because the platform gates none of it:
9
- * every write on these tabs is open to any member of the org.
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';
@@ -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(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: view === 'discover' ? 'Sign in to see what this deployment mounts.' : 'Sign in to see your plugins.' });
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 instanceof Error ? e.message : 'The build failed');
75
+ setNote(e.message);
76
76
  }
77
77
  finally {
78
78
  setWorking(false);
@@ -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 instanceof Error ? e.message : 'That did not work');
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(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your skills." });
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 instanceof Error ? e.message : 'The skill was not saved');
117
+ setNote(e.message);
118
118
  }
119
119
  finally {
120
120
  setWorking(false);
@@ -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?: string;
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?: string;
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. */