@hanzo/build 0.2.15 → 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.
@@ -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/builder.js CHANGED
@@ -28,7 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
28
28
  * left column, never two.
29
29
  */
30
30
  import { SizableText, XStack, YStack } from '@hanzo/gui';
31
- import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
31
+ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu, Search, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
32
32
  import { Button } from '@hanzo/ui';
33
33
  import { SessionRail } from '@hanzo/ui/chat';
34
34
  import { HanzoMark } from '@hanzo/ui/product';
@@ -49,7 +49,6 @@ 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 { Where } from './switch.js';
53
52
  function Shell() {
54
53
  const host = useHost();
55
54
  const t = useTarget();
@@ -72,10 +71,10 @@ function Shell() {
72
71
  return list;
73
72
  }, [recents.value, order]);
74
73
  const screen = r.kind === 'screen' ? r.screen : '';
75
- // The top left: the builder's name, which leads to New, and under it the
76
- // organization switcher once there is an org to switch. Beside the rail from md
77
- // up, beside the drawer's button below it — one place at a time.
78
- const switcher = Boolean(host.person && (host.org || (host.memberships?.length ?? 0) > 0));
74
+ // The top left, as on claude.ai: the builder's name, which leads to New, and under
75
+ // it the search that finds a run. The organization is the account's, so it is
76
+ // switched from the account menu at the foot. Beside the rail from md up, beside
77
+ // the drawer's button below it — one place at a time.
79
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
79
  const links = [
81
80
  { id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
@@ -107,13 +106,14 @@ function Shell() {
107
106
  ];
108
107
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), notice: host.person && !slack ? (_jsx(Slack, { onOpen: () => go(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) })) : undefined, account: host.person
109
108
  ? {
110
- name: host.person.email || host.person.name,
109
+ // Who, and in which organization, as claude.ai says who and on which plan.
110
+ name: [host.person.name || host.person.email, host.org].filter(Boolean).join(' · '),
111
111
  onPress: () => {
112
112
  setDrawer(false);
113
113
  setMenu(true);
114
114
  },
115
115
  }
116
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => go(path({ kind: 'settings', section: 'general' })), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : switcher ? 92 : 48, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(YStack, { position: "absolute", t: 10, l: 12, r: 8, z: 2, gap: "$2", "$max-md": { display: 'none' }, children: [name, switcher ? _jsx(Where, {}) : null] })), 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 }), switcher ? _jsx(Where, {}) : null] }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
116
+ : { 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
117
  }
118
118
  /** Whichever pane the address names, beside a rail — the builder's or the host's. */
119
119
  function Pane({ onStarted }) {
@@ -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. */
@@ -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 }), meta ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta })) : null] })] }), action] }));
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 instanceof Error ? e.message : 'That did not work');
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 }), detail ? (_jsx(SizableText, { size: "$1", color: "$soft", children: detail })) : null] }), children] }));
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's menu offers it. */
21
- menu?: NonNullable<DropdownMenuProps['items']>;
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?: string;
24
+ retry: string;
25
25
  onRetry: () => void;
26
- onHide?: () => void;
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 = [], refused, retry = 'Retry', onRetry, onHide, }) {
18
- // A setup run's work IS the environment, so it opens there.
19
- const [tab, setTab] = useState(mode === 'setup' ? 'environment' : 'terminal');
17
+ export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }) {
18
+ // Until a tab is picked it follows the run's mode, which is known once the run
19
+ // is read: a setup run's work IS the environment, so it opens there.
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" })] }), onHide ? (_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 }) })) : null, _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] })] }));
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
@@ -39,7 +39,7 @@ export function Door({ which, sandbox, live, session }) {
39
39
  if (gone)
40
40
  return;
41
41
  setState('failed');
42
- setWhy(e instanceof Error ? e.message : '');
42
+ setWhy(e.message);
43
43
  });
44
44
  return () => {
45
45
  gone = true;
@@ -63,7 +63,7 @@ export function Environment({ repo, busy }) {
63
63
  return true;
64
64
  }
65
65
  catch (e) {
66
- setNote(e instanceof Error ? e.message : 'That did not work');
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 instanceof Error ? e.message : 'The setup run did not start');
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 instanceof Error ? e.message : 'That did not work');
119
+ setNote(e.message);
120
120
  }
121
121
  finally {
122
122
  setWorking(false);
package/lib/foot.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * The rail's foot: the card that offers Hanzo in Slack, and the menu the account
4
- * row opens — who is signed in, Settings, Usage, the plans, help and logging out.
4
+ * row opens — who is signed in, the organization they act in, Settings, Usage,
5
+ * the plans, help and logging out.
5
6
  * Everything it opens is an address of this page, but help, which is the
6
7
  * documentation.
7
8
  */
8
9
  import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
9
10
  import { Gauge, LifeBuoy, LogOut, Settings, Slack as Mark, Sparkles, X } from '@hanzogui/lucide-icons-2';
10
- import { MenuRow, MenuRule } from '@hanzo/ui/product';
11
+ import { MenuLabel, MenuRow, MenuRule } from '@hanzo/ui/product';
11
12
  import { useHost } from './host.js';
12
13
  import { path } from './route.js';
13
14
  export const DOCS = 'https://docs.hanzo.ai/docs/dev';
@@ -22,11 +23,17 @@ export function Slack({ onOpen, onDismiss }) {
22
23
  */
23
24
  export function Who({ open, onOpenChange, narrow }) {
24
25
  const host = useHost();
26
+ // The organizations this person acts in; choosing one reloads the page scoped to it.
27
+ const orgs = host.memberships ?? (host.org ? [host.org] : []);
25
28
  const go = (p) => {
26
29
  onOpenChange(false);
27
30
  host.go(p);
28
31
  };
29
- return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 6, children: [_jsx(Popover.Anchor, { position: "absolute", l: 8, b: 8, width: narrow ? 48 : 256, height: 36, pointerEvents: "none" }), _jsx(Popover.Content, { role: "menu", "aria-label": "Account", items: "stretch", borderWidth: 1, elevation: "$2", px: "$1", py: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person?.email || host.person?.name || '' }), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", icon: _jsx(Settings, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(MenuRow, { label: "Usage", icon: _jsx(Gauge, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(MenuRow, { label: "View all plans", icon: _jsx(Sparkles, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(MenuRow, { label: "Get help", icon: _jsx(LifeBuoy, { size: 16 }), onPress: () => {
32
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 6, children: [_jsx(Popover.Anchor, { position: "absolute", l: 8, b: 8, width: narrow ? 48 : 256, height: 36, pointerEvents: "none" }), _jsx(Popover.Content, { role: "menu", "aria-label": "Account", items: "stretch", borderWidth: 1, elevation: "$2", px: "$1", py: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person?.email || host.person?.name || '' }), orgs.length ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Organization" }), _jsx(YStack, { role: "radiogroup", "aria-label": "Organizations", gap: "$1", children: orgs.map((o) => (_jsx(MenuRow, { label: o, active: o === host.org, onPress: () => {
33
+ onOpenChange(false);
34
+ if (o !== host.org)
35
+ host.chooseOrg?.(o);
36
+ } }, o))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", icon: _jsx(Settings, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(MenuRow, { label: "Usage", icon: _jsx(Gauge, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(MenuRow, { label: "View all plans", icon: _jsx(Sparkles, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(MenuRow, { label: "Get help", icon: _jsx(LifeBuoy, { size: 16 }), onPress: () => {
30
37
  onOpenChange(false);
31
38
  window.open(DOCS, '_blank', 'noopener,noreferrer');
32
39
  } }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuRow, { label: "Log out", icon: _jsx(LogOut, { size: 16 }), onPress: () => {
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: `${r.author || 'Someone'} · ${verdict(r.state)}${r.at ? ` · ${when(r.at)}` : ''}` }), 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" }))] }));
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, MoreHorizontal, PanelRight, Pencil } from '@hanzogui/lucide-icons-2';
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 || detail.value?.title || (detail.loading ? '' : 'Untitled run');
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 (typeof Notification === 'undefined' || Notification.permission !== 'granted')
93
- return;
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
- return () => n.close();
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, failed) => {
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 instanceof Error ? e.message : failed);
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 (!words)
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
- }, 'Could not reach this run');
172
+ });
148
173
  return;
149
174
  }
150
- void act(() => carry(words), 'The follow-up could not start');
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
- }, 'Could not stop this run');
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
- }, 'Could not pause this run');
190
+ });
166
191
  return;
167
192
  case 'resume':
168
193
  if (held)
169
- void act(() => carry(''), 'The run could not go on');
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
- }, 'Could not resume this run');
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
- }, 'Could not change who can read this run');
192
- const link = record ? story(t, record.org || host.org || '', record.project) : '';
193
- const menu = [
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
- }, 'Could not rename this run');
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
- 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, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, 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, menu.length ? (_jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Manage this run", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: menu })) : null] }), _jsx(Transcript, { cards: shown,
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'), 'The build could not start') : undefined, approving: busy, empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
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: "$4", 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, { px: "$3", py: "$2", justify: "space-between", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Steps" }), _jsx(SizableText, { size: "$1", color: "$soft", children: String(plan.length) })] }), plan.map((s, i) => {
252
- // Only a run still working is on a step.
253
- const current = running && !s.done && plan.findIndex((x) => !x.done) === i;
254
- return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || current ? '$ink' : '$soft', children: s.done ? '✓' : current ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
255
- })] })) : 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
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: running && !(paused && held) ? 'Steer this run' : 'Follow up on this run' })] })] }), 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: menu, 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) => {
267
- if ((e.key ?? e.nativeEvent?.key) === 'Enter')
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
  }
@@ -1,6 +1,7 @@
1
- import type { ReactNode } from 'react';
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 }), onApprove ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' }) })) : null] }, c.key));
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.15",
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).",
package/lib/switch.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function Where(): import("react").JSX.Element;
package/lib/switch.js DELETED
@@ -1,39 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * The rail's top left: the organization, and the projects under it.
4
- *
5
- * The account stays at the foot of the rail. Settings opens that account on
6
- * this page. Choosing an organization reloads this origin so every read is
7
- * scoped to it.
8
- */
9
- import { YStack } from '@hanzo/gui';
10
- import { MenuLabel, MenuRow, MenuRule, OrgSwitcher } from '@hanzo/ui/product';
11
- import { projects } from './api/projects.js';
12
- import { useRead } from './data.js';
13
- import { useHost, useTarget } from './host.js';
14
- import { route } from './route.js';
15
- export function Where() {
16
- const host = useHost();
17
- const t = useTarget();
18
- const signed = Boolean(host.person);
19
- const list = useRead(signed ? () => projects(t) : null, [], [t, signed]);
20
- const here = route(host.path);
21
- const current = host.org || '';
22
- const scope = {
23
- currentOrg: () => current,
24
- setCurrentOrg: (next) => host.chooseOrg?.(next),
25
- isScopedAway: () => false,
26
- hasSelectedOrg: () => Boolean(current),
27
- enterOrg: (next) => host.chooseOrg?.(next),
28
- leaveOrg: () => undefined,
29
- switchOrg: (next) => host.chooseOrg?.(next),
30
- };
31
- const rows = () => Promise.resolve((host.memberships ?? (current ? [current] : [])).map((name) => ({ name, displayName: name })));
32
- return (_jsx(OrgSwitcher, { scope: scope, orgs: (page) => (page === 0 ? rows() : Promise.resolve([])), pageSize: 50, search: false, heading: "Organization", lead: true, sub: here.kind === 'project' ? here.slug : undefined, current: current ? { name: current, displayName: current } : undefined, aria: current ? `Organization ${current}` : 'Organization', footer: (close) => (_jsxs(YStack, { gap: "$1", children: [list.value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Project" }), _jsx(YStack, { role: "radiogroup", "aria-label": "Projects", gap: "$1", children: list.value.slice(0, 12).map((p) => (_jsx(MenuRow, { label: p.name || p.slug, active: here.kind === 'project' && here.slug === p.slug, onPress: () => {
33
- close();
34
- host.go(p.slug);
35
- } }, p.slug))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", onPress: () => {
36
- close();
37
- host.go('-/settings');
38
- } })] })) }));
39
- }