@hanzo/build 0.2.51 → 0.2.53

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 (98) hide show
  1. package/README.md +109 -55
  2. package/lib/api/billing.d.ts +39 -0
  3. package/lib/api/billing.js +41 -0
  4. package/lib/api/coding.d.ts +4 -1
  5. package/lib/api/coding.js +2 -0
  6. package/lib/api/environment.d.ts +12 -0
  7. package/lib/api/environment.js +39 -1
  8. package/lib/api/github.d.ts +42 -4
  9. package/lib/api/github.js +79 -16
  10. package/lib/api/limits.d.ts +69 -0
  11. package/lib/api/limits.js +86 -0
  12. package/lib/api/links.d.ts +42 -0
  13. package/lib/api/links.js +65 -0
  14. package/lib/api/pref.d.ts +5 -3
  15. package/lib/api/pref.js +0 -3
  16. package/lib/api/turn.d.ts +10 -1
  17. package/lib/api/turn.js +16 -4
  18. package/lib/api/work.d.ts +75 -24
  19. package/lib/api/work.js +132 -27
  20. package/lib/builder.d.ts +0 -10
  21. package/lib/builder.js +21 -54
  22. package/lib/choice.d.ts +21 -0
  23. package/lib/choice.js +40 -1
  24. package/lib/customize/agents.d.ts +1 -1
  25. package/lib/customize/agents.js +21 -19
  26. package/lib/customize/connectors.d.ts +1 -1
  27. package/lib/customize/connectors.js +80 -31
  28. package/lib/customize/index.d.ts +2 -0
  29. package/lib/customize/index.js +23 -19
  30. package/lib/customize/load.d.ts +9 -0
  31. package/lib/customize/load.js +22 -0
  32. package/lib/customize/plugins.d.ts +1 -1
  33. package/lib/customize/plugins.js +21 -19
  34. package/lib/customize/say.d.ts +1 -0
  35. package/lib/customize/say.js +38 -0
  36. package/lib/customize/skills.d.ts +1 -1
  37. package/lib/customize/skills.js +23 -17
  38. package/lib/customize/ui.d.ts +28 -11
  39. package/lib/customize/ui.js +29 -21
  40. package/lib/data.d.ts +13 -0
  41. package/lib/data.js +70 -0
  42. package/lib/desk.d.ts +6 -1
  43. package/lib/desk.js +43 -19
  44. package/lib/environment.d.ts +11 -3
  45. package/lib/environment.js +53 -13
  46. package/lib/foot.js +10 -6
  47. package/lib/forge.js +255 -92
  48. package/lib/host.d.ts +1 -1
  49. package/lib/index.d.ts +28 -2
  50. package/lib/index.js +26 -2
  51. package/lib/landing.d.ts +0 -10
  52. package/lib/landing.js +79 -47
  53. package/lib/merge.d.ts +223 -0
  54. package/lib/merge.js +380 -0
  55. package/lib/meter.d.ts +31 -0
  56. package/lib/meter.js +45 -0
  57. package/lib/mind.d.ts +60 -0
  58. package/lib/mind.js +187 -0
  59. package/lib/nav.d.ts +11 -0
  60. package/lib/nav.js +75 -0
  61. package/lib/panel.d.ts +45 -0
  62. package/lib/panel.js +115 -0
  63. package/lib/pick.d.ts +8 -0
  64. package/lib/pick.js +18 -1
  65. package/lib/plan.d.ts +78 -0
  66. package/lib/plan.js +133 -0
  67. package/lib/project.d.ts +16 -1
  68. package/lib/project.js +100 -32
  69. package/lib/prompt.d.ts +75 -0
  70. package/lib/prompt.js +125 -0
  71. package/lib/prose.js +14 -10
  72. package/lib/reply.d.ts +36 -0
  73. package/lib/reply.js +124 -0
  74. package/lib/route.d.ts +28 -4
  75. package/lib/route.js +48 -1
  76. package/lib/run.js +87 -52
  77. package/lib/section.d.ts +1 -1
  78. package/lib/section.js +7 -11
  79. package/lib/settings/billing.js +16 -9
  80. package/lib/settings/code.js +15 -12
  81. package/lib/settings/credits.d.ts +9 -0
  82. package/lib/settings/credits.js +110 -0
  83. package/lib/settings/plan.d.ts +17 -0
  84. package/lib/settings/plan.js +108 -0
  85. package/lib/settings/usage.js +11 -119
  86. package/lib/shelf.js +3 -3
  87. package/lib/standing.d.ts +44 -0
  88. package/lib/standing.js +64 -0
  89. package/lib/sync.js +23 -9
  90. package/lib/tabs.d.ts +53 -0
  91. package/lib/tabs.js +197 -0
  92. package/lib/tools.d.ts +2 -9
  93. package/lib/tools.js +20 -25
  94. package/lib/transcript.d.ts +10 -1
  95. package/lib/transcript.js +25 -10
  96. package/lib/voice.d.ts +2 -13
  97. package/lib/voice.js +7 -73
  98. package/package.json +20 -3
package/lib/api/work.js CHANGED
@@ -1,19 +1,33 @@
1
1
  /**
2
- * Projects and issues, as the forge holds them.
2
+ * The work an organization does: its projects, their issues, and its boards.
3
3
  *
4
- * A project is a board. A board is a repository that has work on it, so the
5
- * list is the forge's answer and not a second copy of it.
4
+ * GET /v1/provider/github/repos?q=&owner=&limit=&after= what the GitHub connection grants, most recently pushed first
5
+ * GET /v1/provider/github/installations the GitHub accounts it grants them from
6
+ * GET /v1/git/repos the forge's repositories
7
+ * GET /v1/task/board every issue and pull request: the forge's, and the task index's
8
+ * GET /v1/task/projects the boards
9
+ * GET /v1/task/projects/{key}/issues one board's
6
10
  *
7
- * GET /v1/task/projects the boards
8
- * GET /v1/task/board every board's issues
9
- * GET /v1/task/projects/{key}/issues one board's issues
11
+ * A project is a repository, so the list is those sources merged (merge.ts) and
12
+ * not a second copy of them. An issue row from the board is read for what it
13
+ * is: a forge issue names its forge repository and links to its page; a GitHub
14
+ * issue the index mirrors names its address only in `extRef`
15
+ * (`github:<owner>/<name>#<n>`), and its `number` there is the board's, not
16
+ * GitHub's; any other index row's `repo` is what its writer said — a run's
17
+ * repository, or a Linear team's key — and is resolved, never used raw.
10
18
  *
11
19
  * Creating, renaming and deleting a board are forge operations. This surface
12
20
  * reads the work and opens a run against it.
13
21
  */
14
- import { call, seg } from './call.js';
22
+ import { call } from './call.js';
23
+ import { codebases } from './codebases.js';
24
+ import { installations, repos as granted } from './github.js';
25
+ import { assemble, canon, issues as merged, owners } from '../merge.js';
26
+ import { links } from './links.js';
27
+ import { projects as built } from './projects.js';
15
28
  const str = (v) => (typeof v === 'string' ? v : '');
16
- const obj = (v) => v && typeof v === 'object' ? v : {};
29
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
30
+ const obj = (v) => (v && typeof v === 'object' ? v : {});
17
31
  function rows(raw) {
18
32
  if (Array.isArray(raw))
19
33
  return raw;
@@ -31,37 +45,128 @@ export function board(raw) {
31
45
  return null;
32
46
  return { id: str(o.id) || key, key, name: str(o.name) || key, description: str(o.description) };
33
47
  }
48
+ /** Unix seconds as RFC 3339, or ''. */
49
+ const instant = (s) => (s > 0 ? new Date(s * 1000).toISOString() : '');
50
+ const FINISHED = new Set(['done', 'canceled']);
51
+ const GITHUB = /^github:([A-Za-z0-9][A-Za-z0-9-]*)\/([A-Za-z0-9._-]+)#(\d+)$/;
52
+ /** Where an index row came from, by the reference its writer keyed it with. */
53
+ function origin(ext) {
54
+ if (ext.startsWith('github:'))
55
+ return 'github';
56
+ if (ext.startsWith('linear:'))
57
+ return 'linear';
58
+ if (ext.startsWith('agent:'))
59
+ return 'agent';
60
+ return 'board';
61
+ }
62
+ /** One board row as an issue: what it is, where it lives, and which repository it says it is for. */
34
63
  export function work(raw) {
35
64
  const o = obj(raw);
36
65
  const title = str(o.title);
37
- const project = str(o.projectKey);
38
- const number = typeof o.number === 'number' ? o.number : 0;
39
- if (!title && !project)
66
+ const key = str(o.projectKey);
67
+ const n = num(o.number);
68
+ if (!title && !key)
40
69
  return null;
70
+ const ext = str(o.extRef);
71
+ const gh = ext.match(GITHUB);
72
+ const page = canon(str(o.url));
73
+ // A forge row links to its own page and carries no reference of another host's.
74
+ const forge = !ext && page ? str(o.repo) : '';
75
+ const url = gh ? `https://github.com/${gh[1]}/${gh[2]}/issues/${gh[3]}` : page;
76
+ const assignee = str(o.assignee);
41
77
  return {
42
- id: str(o.id) || `${project}#${number}`,
43
- identifier: str(o.identifier) || (project && number ? `${project}#${number}` : ''),
44
- project,
45
- number,
46
- kind: str(o.kind) || 'issue',
78
+ key: canon(url) || (key && n ? `${key}#${n}`.toLowerCase() : '') || str(o.id),
79
+ repo: gh ? `${gh[1]}/${gh[2]}` : '',
80
+ forge,
81
+ // A run's repository, or what a writer of the index named; a Linear row's `repo` is its team's key and never one.
82
+ hint: gh || forge || origin(ext) === 'linear' ? '' : str(o.repo),
83
+ number: gh ? Number(gh[3]) : n,
47
84
  title: title || 'Untitled',
85
+ body: str(o.description),
86
+ state: FINISHED.has(str(o.status)) ? 'closed' : 'open',
87
+ labels: (Array.isArray(o.labels) ? o.labels : []).filter((l) => typeof l === 'string' && l.trim() !== ''),
88
+ assignees: assignee ? [assignee] : [],
89
+ url,
90
+ sources: [forge ? 'forge' : origin(ext)],
91
+ updated: instant(num(o.updatedAt)) || instant(num(o.createdAt)),
92
+ created: instant(num(o.createdAt)),
93
+ kind: str(o.kind) || 'issue',
48
94
  status: str(o.status) || 'backlog',
49
95
  priority: str(o.priority) || 'none',
50
- assignee: str(o.assignee),
51
- repo: str(o.repo),
96
+ pull: str(o.kind) === 'pr',
97
+ board: key,
52
98
  };
53
99
  }
54
- export async function boards(t) {
55
- const raw = await call(t, 'GET', '/v1/task/projects');
100
+ export async function boards(t, signal) {
101
+ const raw = await call(t, 'GET', '/v1/task/projects', undefined, { signal });
56
102
  return rows(raw)
57
103
  .map(board)
58
104
  .filter((b) => b !== null);
59
105
  }
60
- /** A board's issues, or every board's when `key` is empty. */
61
- export async function issues(t, key = '') {
62
- const path = key ? `/v1/task/projects/${seg(key)}/issues` : '/v1/task/board';
63
- const raw = await call(t, 'GET', path);
64
- return rows(raw)
65
- .map(work)
66
- .filter((w) => w !== null);
106
+ const issuesOf = (raw) => merged(rows(raw)
107
+ .map(work)
108
+ .filter((w) => w !== null));
109
+ /** Every issue and pull request the org has, from the forge and the task index, each once. */
110
+ export async function everything(t, signal) {
111
+ return issuesOf(await call(t, 'GET', '/v1/task/board', undefined, { signal }));
112
+ }
113
+ /**
114
+ * What the org's projects are made from, each read on its own: the links (GET
115
+ * /v1/sync), the forge's repositories, the org's projects, and GitHub's grants
116
+ * — searched and paged by GitHub, and empty for an operator with no GitHub of
117
+ * their own, which is why it is never the only source. One that fails leaves the
118
+ * others standing and is named in `missing`. A next page reads GitHub alone.
119
+ */
120
+ export async function sources(t, p = {}, signal) {
121
+ const missing = [];
122
+ const soft = (what, read) => read.catch((e) => {
123
+ if (signal?.aborted)
124
+ throw e;
125
+ missing.push(what);
126
+ return null;
127
+ });
128
+ const first = !p.after;
129
+ const [hub, linked, forge, sites, accounts] = await Promise.all([
130
+ soft('GitHub', granted(t, { q: p.q, owner: p.owner, limit: 100, after: p.after }, signal)),
131
+ first ? soft('the linked repositories', links(t, signal)) : null,
132
+ first ? soft('the forge', codebases(t)) : null,
133
+ first ? soft('the projects', built(t)) : null,
134
+ first ? installations(t).catch(() => []) : [],
135
+ ]);
136
+ return {
137
+ hub: hub?.repos ?? [],
138
+ linked: linked ?? [],
139
+ held: (forge ?? []).map((c) => ({ org: c.org, name: c.name, description: c.description, branch: c.branch, public: c.public, updated: c.updated })),
140
+ sites: (sites ?? []).filter((x) => x.repo).map((x) => ({ slug: x.slug, repo: x.repo, branch: x.branch, updated: x.updated ? new Date(x.updated * 1000).toISOString() : '' })),
141
+ owners: owners(accounts.map((a) => a.login), (linked ?? []).map((l) => l.owner)),
142
+ next: hub?.next ?? '',
143
+ unread: hub?.unread ?? [],
144
+ missing,
145
+ };
146
+ }
147
+ /**
148
+ * Every repository on the forge a run can work on: the forge's own and every
149
+ * linked repository's copy (GET /v1/git/repos does not list an estate's copies;
150
+ * GET /v1/sync does). Throws only when neither answered.
151
+ */
152
+ export async function homes(t, signal) {
153
+ const failed = [];
154
+ const soft = (read) => read.catch((e) => {
155
+ failed.push(e);
156
+ return null;
157
+ });
158
+ const [linked, forge] = await Promise.all([soft(links(t, signal)), soft(codebases(t))]);
159
+ if (!linked && !forge)
160
+ throw failed[0];
161
+ const list = assemble({
162
+ linked: linked ?? [],
163
+ held: (forge ?? []).map((c) => ({ org: c.org, name: c.name, description: c.description, branch: c.branch, public: c.public, updated: c.updated })),
164
+ });
165
+ return {
166
+ homes: list
167
+ .filter((p) => p.forge)
168
+ .map((p) => ({ name: p.forge, label: `${p.owner}/${p.name}`, branch: p.branch || 'main' }))
169
+ .sort((a, b) => a.label.localeCompare(b.label)),
170
+ whole: failed.length === 0,
171
+ };
67
172
  }
package/lib/builder.d.ts CHANGED
@@ -1,14 +1,4 @@
1
- import { type RailLink } from '@hanzo/ui/chat';
2
1
  import { type Host } from './host.tsx';
3
- /**
4
- * The builder's places, as a rail draws them: four in view and the rest under
5
- * More. Every one moves the builder through `go`, so a host that draws its own
6
- * rail lists the same places, in the same words, as the builder's rail does.
7
- */
8
- export declare function nav(host: Host, go: (path: string) => void): {
9
- links: RailLink[];
10
- more: RailLink[];
11
- };
12
2
  /**
13
3
  * The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
14
4
  *
package/lib/builder.js CHANGED
@@ -5,42 +5,42 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * / New — the empty state, a composer over the codebase it works on
6
6
  * /sess_… one run, live
7
7
  * /-/automations work that runs itself; /-/automations/<id> one, to edit and run
8
- /-/codebases the forge's repositories
9
- * /-/sync bring granted repositories onto the forge
10
- * /-/projects the forge's boards
8
+ * /-/projects the org's repositories on the forge: a project is a repository
9
+ * /-/sync link repositories from GitHub onto the forge
11
10
  * /-/issues the forge's issues
12
11
  * /-/artifacts what this org has built
13
12
  * /-/templates the public starters
14
13
  * /-/customize skills, connectors, plugins and agents
15
- * /<slug> a deployed project's workspace, in this same window
14
+ * /<org>/<repo> a repository's workspace, in this same window
15
+ * /<slug> a deployed site's workspace
16
16
  *
17
- * The rail is the sessions rail every Hanzo surface shares: New, the builder's
18
- * places with the rest under More, the org's coding runs newest first with a
17
+ * The rail is the sessions rail every Hanzo surface shares: New, every one of
18
+ * the builder's places (nav.tsx), the org's coding runs newest first with a
19
19
  * live status dot, and at the foot the offer of Hanzo in Slack and the account,
20
20
  * whose menu opens Settings, Usage, the plans, help and logging out (foot.tsx).
21
21
  * Its collapse is an explicit toggle kept per browser.
22
22
  *
23
- * A project's workspace takes the whole window — it has its own chat column,
24
- * bar and panes — and its mark leads back here.
23
+ * A workspace takes the whole window — it has its own chat column, bar and
24
+ * panes — and its mark leads back here.
25
25
  *
26
26
  * A host with a rail of its own mounts `<Builder rail={false}>` and draws the
27
- * builder's places (`nav`) and runs in that rail (`useSessions`, or `DevSection`,
28
- * section.tsx): one left column, never two.
27
+ * builder's places (`nav`, nav.tsx) and runs in that rail (`useSessions`, or
28
+ * `DevSection`, section.tsx): one 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, Search, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
31
+ import { Menu, Search } 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';
35
35
  import { useMemo, useState } from 'react';
36
36
  import { safe } from './api/projects.js';
37
- import { pinBoard } from './choice.js';
38
37
  import { Customize } from './customize/index.js';
39
38
  import { Editor } from './automations.js';
40
39
  import { useKept, useRecents } from './data.js';
41
40
  import { Forge } from './forge.js';
42
41
  import { HostProvider, useHost, useTarget } from './host.js';
43
42
  import { Landing } from './landing.js';
43
+ import { nav, NEW } from './nav.js';
44
44
  import { PrefsProvider } from './prefs.js';
45
45
  import { Project } from './project.js';
46
46
  import { Plans } from './plans.js';
@@ -49,7 +49,7 @@ import { Run } from './run.js';
49
49
  import { DOTS } from './section.js';
50
50
  import { Artifacts, Templates } from './shelf.js';
51
51
  import { Find } from './find.js';
52
- import { DOCS, Slack, useWho, Who } from './foot.js';
52
+ import { Slack, useWho, Who } from './foot.js';
53
53
  import { Settings } from './settings/index.js';
54
54
  function Shell() {
55
55
  const host = useHost();
@@ -77,8 +77,11 @@ function Shell() {
77
77
  // switched from the account menu at the foot. Beside the rail from md up, beside
78
78
  // the drawer's button below it — one place at a time.
79
79
  const name = _jsx(Name, { onPress: () => go('') });
80
- const { links, more } = nav(host, go);
81
- 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
80
+ // Every place in view, none behind a More; the rail's own New starts a run.
81
+ const links = nav(host, go)
82
+ .flatMap((g) => g.links)
83
+ .filter((l) => l.id !== NEW);
84
+ 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, 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
82
85
  ? {
83
86
  // Who, and in which organization, as claude.ai says who and on which plan.
84
87
  name: [host.person.name || host.person.email, host.org].filter(Boolean).join(' · '),
@@ -89,44 +92,6 @@ function Shell() {
89
92
  }
90
93
  : { name: 'Sign in', onPress: () => host.signIn?.() }, collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org, logo: host.logo }), 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: who.open, onOpenChange: who.onOpenChange, 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); } })] }));
91
94
  }
92
- /**
93
- * The builder's places, as a rail draws them: four in view and the rest under
94
- * More. Every one moves the builder through `go`, so a host that draws its own
95
- * rail lists the same places, in the same words, as the builder's rail does.
96
- */
97
- export function nav(host, go) {
98
- const r = route(host.path);
99
- const screen = r.kind === 'screen' ? r.screen : '';
100
- const links = [
101
- { id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
102
- { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
103
- { id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: () => go(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
104
- { id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' || r.kind === 'automation' },
105
- ];
106
- const more = [
107
- { id: 'codebases', label: 'Codebase', icon: _jsx(FolderGit2, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), active: screen === 'codebases' },
108
- {
109
- id: 'issues',
110
- label: 'Issues',
111
- icon: _jsx(CircleDot, { size: 16 }),
112
- onPress: () => {
113
- pinBoard(host.org, '');
114
- go(path({ kind: 'screen', screen: 'issues' }));
115
- },
116
- active: screen === 'issues',
117
- },
118
- { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
119
- {
120
- id: 'machines',
121
- label: 'Machines',
122
- icon: _jsx(Cpu, { size: 16 }),
123
- onPress: () => go(path({ kind: 'settings', section: 'machines' })),
124
- active: r.kind === 'settings' && r.section === 'machines',
125
- },
126
- { id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
127
- ];
128
- return { links, more };
129
- }
130
95
  /** The wordmark: the product's name, as its host calls it, which leads to New. */
131
96
  function Name({ onPress }) {
132
97
  const { name } = useHost();
@@ -172,7 +137,9 @@ function Screens({ rail }) {
172
137
  const host = useHost();
173
138
  const r = route(host.path);
174
139
  if (r.kind === 'project')
175
- return _jsx(Project, { slug: r.slug }, r.slug);
140
+ return _jsx(Project, { at: { slug: r.slug } }, r.slug);
141
+ if (r.kind === 'repo')
142
+ return _jsx(Project, { at: { org: r.org, name: r.name } }, `${r.org}/${r.name}`);
176
143
  if (!rail)
177
144
  return _jsx(Pane, {});
178
145
  return _jsx(Shell, {});
package/lib/choice.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * model and the machine; it only replaces the fields the caller names.
7
7
  */
8
8
  import { type Codebase, type ForgeRepo } from './api/codebases.ts';
9
+ import type { Project } from './merge.ts';
9
10
  export interface Choice {
10
11
  repo: ForgeRepo | null;
11
12
  branch: string;
@@ -15,12 +16,32 @@ export interface Choice {
15
16
  effort: string;
16
17
  /** A draft handed over by another screen. New copies it once and clears it. */
17
18
  ask: string;
19
+ /** The issue that draft is, by the number the run's pull request closes; 0 for none. Handed over with it. */
20
+ issue: number;
18
21
  }
19
22
  export declare const boardKey: (org: string | null) => string;
23
+ /**
24
+ * The choice New opens on, field by field: the last layer that says something
25
+ * wins. What was kept here, over the person's coding defaults, over the first
26
+ * run's. A field kept as '' stays '' (the default branch, the sandbox): only a
27
+ * field that is absent falls through, so a codebase or an issue handed over from
28
+ * its own screen — which keeps the codebase and the words alone — still opens on
29
+ * a model, a mode and an effort.
30
+ */
31
+ export declare function settle<T extends object>(first: T, ...layers: (Partial<T> | null | undefined)[]): T;
20
32
  /** Point New at a forge codebase, and optionally a first draft. */
21
33
  export declare function pinCodebase(org: string | null, c: Codebase, ask?: string): void;
22
34
  /** Which board Issues is showing. '' is every board. */
23
35
  export declare function pinBoard(org: string | null, key: string): void;
36
+ /**
37
+ * Point New at a project, and optionally a first draft (an issue's ask) and the
38
+ * issue it closes. `where` is where the run works (merge.ts `home`): on the forge
39
+ * it is chosen by its forge name, which is what a run and its environment
40
+ * address; on GitHub by GitHub's address, so the run clones and proposes there.
41
+ * Left out, the forge when the project is there. A project that is neither
42
+ * cannot be worked on, and is refused here rather than handed to the platform.
43
+ */
44
+ export declare function pinRepo(org: string | null, p: Project, ask?: string, issue?: number, where?: 'github' | 'forge' | null): void;
24
45
  /**
25
46
  * A repository on GitHub, chosen from the org's installation: the run works there.
26
47
  * Its `full_name` is GitHub's address, `github.com/owner/name` — what the list
package/lib/choice.js CHANGED
@@ -26,10 +26,27 @@ function write(org, value) {
26
26
  /* the next New starts empty */
27
27
  }
28
28
  }
29
+ /**
30
+ * The choice New opens on, field by field: the last layer that says something
31
+ * wins. What was kept here, over the person's coding defaults, over the first
32
+ * run's. A field kept as '' stays '' (the default branch, the sandbox): only a
33
+ * field that is absent falls through, so a codebase or an issue handed over from
34
+ * its own screen — which keeps the codebase and the words alone — still opens on
35
+ * a model, a mode and an effort.
36
+ */
37
+ export function settle(first, ...layers) {
38
+ let out = { ...first };
39
+ for (const layer of layers) {
40
+ if (!layer || typeof layer !== 'object' || Array.isArray(layer))
41
+ continue;
42
+ out = { ...out, ...Object.fromEntries(Object.entries(layer).filter(([, v]) => v !== undefined && v !== null)) };
43
+ }
44
+ return out;
45
+ }
29
46
  /** Point New at a forge codebase, and optionally a first draft. */
30
47
  export function pinCodebase(org, c, ask = '') {
31
48
  const repo = asRepo(c);
32
- write(org, { ...read(org), repo, branch: repo.default_branch, ask });
49
+ write(org, { ...read(org), repo, branch: repo.default_branch, ask, issue: 0 });
33
50
  }
34
51
  /** Which board Issues is showing. '' is every board. */
35
52
  export function pinBoard(org, key) {
@@ -41,6 +58,28 @@ export function pinBoard(org, key) {
41
58
  }
42
59
  window.dispatchEvent(new CustomEvent('hanzo-board', { detail: key }));
43
60
  }
61
+ /**
62
+ * Point New at a project, and optionally a first draft (an issue's ask) and the
63
+ * issue it closes. `where` is where the run works (merge.ts `home`): on the forge
64
+ * it is chosen by its forge name, which is what a run and its environment
65
+ * address; on GitHub by GitHub's address, so the run clones and proposes there.
66
+ * Left out, the forge when the project is there. A project that is neither
67
+ * cannot be worked on, and is refused here rather than handed to the platform.
68
+ */
69
+ export function pinRepo(org, p, ask = '', issue = 0, where = p.forge ? 'forge' : p.linked ? 'github' : null) {
70
+ const branch = p.branch || 'main';
71
+ let repo;
72
+ if (where === 'forge' && p.forge) {
73
+ repo = { owner: org ?? '', name: p.forge, full_name: org ? `${org}/${p.forge}` : p.forge, private: p.private ?? true, default_branch: branch, pushed_at: '', installation_id: 0, forge: true, clone: '' };
74
+ }
75
+ else if (where === 'github' && p.linked) {
76
+ repo = asHub({ owner: p.owner, name: p.name, full_name: `${p.owner}/${p.name}`, private: p.private ?? true, default_branch: branch, pushed_at: '', installation_id: 0 });
77
+ }
78
+ else {
79
+ throw new Error(`${p.owner}/${p.name} is neither on the forge nor linked from GitHub`);
80
+ }
81
+ write(org, { ...read(org), repo, branch, ask, issue });
82
+ }
44
83
  export const isHub = (repo) => Boolean(repo && repo.github === true && repo.full_name?.startsWith('github.com/'));
45
84
  /** A GitHub listing row as the composer offers it. */
46
85
  export const asHub = (r) => ({ ...r, full_name: `github.com/${r.owner}/${r.name}`, github: true });
@@ -1,2 +1,2 @@
1
1
  import { type Pane } from './ui.tsx';
2
- export declare function Agents({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;
2
+ export declare function Agents({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;
@@ -3,12 +3,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  * Agents: the org's own — a model, instructions, the tools it may call, and
4
4
  * what it may spend.
5
5
  *
6
- * Yours lists them; opening one edits it in place, and only what changed is
7
- * sent. An agent calls only the tools it names, and only those that are on for
8
- * the org, so the choices are the tools that are on; "every tool" is whatever
9
- * the fleet's MCP server serves when it runs. A budget is required: what it may
10
- * spend each period, and in one run. Discover is the platform's presets whose
11
- * tool calls run on the platform; adding one opens a new agent written from it.
6
+ * Browse is the platform's presets whose tool calls run on the platform;
7
+ * starting from one opens a new agent written from it. Yours lists the org's
8
+ * agents; opening one edits it in place, and only what changed is sent. An
9
+ * agent calls only the tools it names, and only those that are on for the org,
10
+ * so the choices are the tools that are on; "every tool" is whatever the
11
+ * fleet's MCP server serves when it runs. A budget is required: what it may
12
+ * spend each period, and in one run.
12
13
  */
13
14
  import { SizableText, XStack, YStack } from '@hanzo/gui';
14
15
  import { Bot } from '@hanzogui/lucide-icons-2';
@@ -18,17 +19,18 @@ import { useEffect, useMemo, useState } from 'react';
18
19
  import { agents, ALL, create, draft, EMPTY, fromPreset, one, PERIODS, presets, remove, update } from '../api/agents.js';
19
20
  import { models } from '../api/models.js';
20
21
  import { tools } from '../api/tools.js';
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, Visitor } from './ui.js';
24
- /** The model a new agent runs on when none is chosen: the deployment's own default. */
23
+ import { useLoad } from './load.js';
24
+ import { say } from './say.js';
25
+ import { Add, Choice, Confirm, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
25
26
  const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')}`;
26
- export function Agents({ view, q, adding, onAdding, onView }) {
27
+ export function Agents({ view, q, adding, onAdding, onView, onCount }) {
27
28
  const host = useHost();
28
29
  const t = useTarget();
29
30
  const signed = Boolean(host.person);
30
- const mine = useRead(signed ? () => agents(t) : null, [], [t, signed]);
31
- const offered = useRead(view === 'discover' ? () => presets(t) : null, [], [t, view]);
31
+ const mine = useLoad(signed ? () => agents(t) : null, [], [t, signed]);
32
+ const offered = useLoad(view === 'discover' ? () => presets(t) : null, [], [t, view]);
33
+ useCount(mine.value.length, onCount);
32
34
  const [opened, setOpened] = useState(null);
33
35
  const [start, setStart] = useState(null);
34
36
  const [note, setNote] = useState('');
@@ -53,13 +55,13 @@ export function Agents({ view, q, adding, onAdding, onView }) {
53
55
  if (view === 'discover') {
54
56
  const names = new Set(mine.value.map((a) => a.name));
55
57
  const shown = offered.value.filter((p) => matches(q, p.id, p.title, p.prompt));
56
- 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
+ return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), _jsx(Part, { title: "Start from a preset", detail: "A ready agent from Hanzo. You can change it before you save it.", children: offered.error && !offered.value.length ? (_jsx(Failed, { error: offered.error, onRetry: offered.reload })) : offered.loading && !offered.value.length ? (_jsx(Soft, { children: "Loading presets\u2026" })) : !shown.length ? (_jsx(Soft, { children: offered.value.length ? `No preset matches “${q.trim()}”.` : 'There are no presets yet. Start from a blank agent with New agent.' })) : (_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, label: "Use", added: names.has(p.id), onPress: () => setStart(fromPreset(p)) }) : undefined }, p.id))) })) }), editor] }));
57
59
  }
58
60
  if (!signed) {
59
61
  return _jsx(Visitor, { children: "Sign in to see your agents." });
60
62
  }
61
63
  const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
62
- 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] }));
64
+ return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail: "Start from a preset, or give a new one a model, instructions and tools.", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse presets" }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "New agent" })] })) : !shown.length ? (_jsx(Soft, { children: `No agent of yours matches “${q.trim()}”.` })) : (_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] }));
63
65
  function reach(a) {
64
66
  if (a.tools.includes(ALL))
65
67
  return 'every tool';
@@ -69,9 +71,9 @@ export function Agents({ view, q, adding, onAdding, onView }) {
69
71
  /** Write a new agent, or change one; an existing agent is read whole first, for its instructions. */
70
72
  function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
71
73
  const t = useTarget();
72
- const full = useRead(agent ? () => one(t, agent.id || agent.name) : null, null, [t, agent?.id]);
73
- const catalog = useRead(() => models(t), [], [t]);
74
- const on = useRead(() => tools(t, { activated: true }), [], [t]);
74
+ const full = useLoad(agent ? () => one(t, agent.id || agent.name) : null, null, [t, agent?.id]);
75
+ const catalog = useLoad(() => models(t), [], [t]);
76
+ const on = useLoad(() => tools(t, { activated: true }), [], [t]);
75
77
  const [d, setD] = useState(start ?? EMPTY);
76
78
  const [ready, setReady] = useState(!agent);
77
79
  const [working, setWorking] = useState(false);
@@ -103,14 +105,14 @@ function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
103
105
  onSaved(saved);
104
106
  }
105
107
  catch (e) {
106
- setNote(e.message);
108
+ setNote(say(e));
107
109
  }
108
110
  finally {
109
111
  setWorking(false);
110
112
  }
111
113
  };
112
114
  const pick = (name, yes) => set({ tools: yes ? [...d.tools, name] : d.tools.filter((n) => n !== name) });
113
- return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: "Model", placeholder: "The deployment\u2019s default", models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error?.message ?? null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: "Use the default" })) : null] }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "Instructions", rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Reading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Soft, { children: on.error.message })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization. Switch skills and connectors on first.' })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
115
+ return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Failed, { error: full.error, onRetry: full.reload })) : !ready ? (_jsx(Soft, { children: "Loading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: "Model", placeholder: "The deployment\u2019s default", models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error ? say(catalog.error) : null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: "Use the default" })) : null] }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "Instructions", rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Loading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Failed, { error: on.error, onRetry: on.reload })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization yet. Add skills or connectors first.' })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
114
116
  await remove(t, agent.id || agent.name);
115
117
  onDeleted();
116
118
  } })) : null] })) }));
@@ -1,2 +1,2 @@
1
1
  import { type Pane } from './ui.tsx';
2
- export declare function Connectors({ view, q, adding, onAdding, onView }: Pane): import("react").JSX.Element;
2
+ export declare function Connectors({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;