@hanzo/build 0.2.52 → 0.2.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -49
- package/lib/api/billing.d.ts +39 -0
- package/lib/api/billing.js +41 -0
- package/lib/api/coding.d.ts +4 -1
- package/lib/api/coding.js +2 -0
- package/lib/api/environment.d.ts +12 -0
- package/lib/api/environment.js +39 -1
- package/lib/api/github.d.ts +24 -4
- package/lib/api/github.js +60 -16
- package/lib/api/limits.d.ts +69 -0
- package/lib/api/limits.js +86 -0
- package/lib/api/links.d.ts +42 -0
- package/lib/api/links.js +65 -0
- package/lib/api/pref.d.ts +5 -3
- package/lib/api/pref.js +0 -3
- package/lib/api/turn.d.ts +10 -1
- package/lib/api/turn.js +16 -4
- package/lib/api/work.d.ts +75 -38
- package/lib/api/work.js +132 -44
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +12 -52
- package/lib/choice.d.ts +21 -0
- package/lib/choice.js +40 -1
- package/lib/customize/agents.d.ts +1 -1
- package/lib/customize/agents.js +21 -19
- package/lib/customize/connectors.d.ts +1 -1
- package/lib/customize/connectors.js +80 -31
- package/lib/customize/index.d.ts +2 -0
- package/lib/customize/index.js +23 -19
- package/lib/customize/load.d.ts +9 -0
- package/lib/customize/load.js +22 -0
- package/lib/customize/plugins.d.ts +1 -1
- package/lib/customize/plugins.js +21 -19
- package/lib/customize/say.d.ts +1 -0
- package/lib/customize/say.js +38 -0
- package/lib/customize/skills.d.ts +1 -1
- package/lib/customize/skills.js +23 -17
- package/lib/customize/ui.d.ts +28 -11
- package/lib/customize/ui.js +29 -21
- package/lib/data.d.ts +13 -0
- package/lib/data.js +70 -0
- package/lib/desk.d.ts +6 -1
- package/lib/desk.js +43 -19
- package/lib/environment.d.ts +11 -3
- package/lib/environment.js +53 -13
- package/lib/foot.js +10 -6
- package/lib/forge.js +139 -71
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +70 -38
- package/lib/merge.d.ts +223 -0
- package/lib/merge.js +380 -0
- package/lib/meter.d.ts +31 -0
- package/lib/meter.js +45 -0
- package/lib/mind.d.ts +60 -0
- package/lib/mind.js +187 -0
- package/lib/nav.d.ts +11 -0
- package/lib/nav.js +75 -0
- package/lib/panel.d.ts +46 -0
- package/lib/panel.js +116 -0
- package/lib/pick.d.ts +8 -0
- package/lib/pick.js +18 -1
- package/lib/plan.d.ts +78 -0
- package/lib/plan.js +133 -0
- package/lib/project.js +16 -6
- package/lib/prompt.d.ts +75 -0
- package/lib/prompt.js +125 -0
- package/lib/prose.js +14 -10
- package/lib/reply.d.ts +36 -0
- package/lib/reply.js +124 -0
- package/lib/route.d.ts +16 -0
- package/lib/route.js +40 -0
- package/lib/run.js +87 -52
- package/lib/section.d.ts +1 -1
- package/lib/section.js +7 -11
- package/lib/settings/billing.js +16 -9
- package/lib/settings/code.js +15 -12
- package/lib/settings/credits.d.ts +9 -0
- package/lib/settings/credits.js +110 -0
- package/lib/settings/plan.d.ts +17 -0
- package/lib/settings/plan.js +108 -0
- package/lib/settings/usage.js +11 -119
- package/lib/shelf.js +3 -3
- package/lib/standing.d.ts +44 -0
- package/lib/standing.js +64 -0
- package/lib/sync.js +22 -8
- package/lib/tabs.d.ts +53 -0
- package/lib/tabs.js +197 -0
- package/lib/tools.d.ts +2 -9
- package/lib/tools.js +20 -25
- package/lib/transcript.d.ts +10 -1
- package/lib/transcript.js +25 -10
- package/lib/voice.d.ts +2 -13
- package/lib/voice.js +7 -73
- package/package.json +20 -3
package/lib/api/work.js
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The work an organization does: its projects, their issues, and its boards.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function inProject(w, key) {
|
|
21
|
-
const k = key.toLowerCase();
|
|
22
|
-
return w.repo.toLowerCase() === k || w.project.toLowerCase() === k;
|
|
23
|
-
}
|
|
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';
|
|
24
28
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const https = (v) => (typeof v === 'string' && /^https:\/\//.test(v) ? v : '');
|
|
28
|
-
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 : {});
|
|
29
31
|
function rows(raw) {
|
|
30
32
|
if (Array.isArray(raw))
|
|
31
33
|
return raw;
|
|
@@ -43,42 +45,128 @@ export function board(raw) {
|
|
|
43
45
|
return null;
|
|
44
46
|
return { id: str(o.id) || key, key, name: str(o.name) || key, description: str(o.description) };
|
|
45
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. */
|
|
46
63
|
export function work(raw) {
|
|
47
64
|
const o = obj(raw);
|
|
48
65
|
const title = str(o.title);
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
if (!title && !
|
|
66
|
+
const key = str(o.projectKey);
|
|
67
|
+
const n = num(o.number);
|
|
68
|
+
if (!title && !key)
|
|
52
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);
|
|
53
77
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
59
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',
|
|
60
94
|
status: str(o.status) || 'backlog',
|
|
61
95
|
priority: str(o.priority) || 'none',
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
source: str(o.source),
|
|
65
|
-
labels: Array.isArray(o.labels) ? o.labels.filter((l) => typeof l === 'string' && l !== '') : [],
|
|
66
|
-
url: https(o.url) || https(o.extRef),
|
|
67
|
-
created: when(o.createdAt),
|
|
68
|
-
updated: when(o.updatedAt),
|
|
96
|
+
pull: str(o.kind) === 'pr',
|
|
97
|
+
board: key,
|
|
69
98
|
};
|
|
70
99
|
}
|
|
71
|
-
export async function boards(t) {
|
|
72
|
-
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 });
|
|
73
102
|
return rows(raw)
|
|
74
103
|
.map(board)
|
|
75
104
|
.filter((b) => b !== null);
|
|
76
105
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
};
|
|
84
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
|
@@ -14,8 +14,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
* /<org>/<repo> a repository's workspace, in this same window
|
|
15
15
|
* /<slug> a deployed site's workspace
|
|
16
16
|
*
|
|
17
|
-
* The rail is the sessions rail every Hanzo surface shares: New,
|
|
18
|
-
*
|
|
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.
|
|
@@ -24,23 +24,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
81
|
-
|
|
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,49 +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
|
-
{
|
|
102
|
-
id: 'projects',
|
|
103
|
-
label: 'Projects',
|
|
104
|
-
icon: _jsx(FolderGit2, { size: 16 }),
|
|
105
|
-
onPress: () => go(path({ kind: 'screen', screen: 'projects' })),
|
|
106
|
-
active: screen === 'projects' || screen === 'sync' || r.kind === 'repo',
|
|
107
|
-
},
|
|
108
|
-
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
109
|
-
{ id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: () => go(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
|
|
110
|
-
{ id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' || r.kind === 'automation' },
|
|
111
|
-
];
|
|
112
|
-
const more = [
|
|
113
|
-
{
|
|
114
|
-
id: 'issues',
|
|
115
|
-
label: 'Issues',
|
|
116
|
-
icon: _jsx(CircleDot, { size: 16 }),
|
|
117
|
-
onPress: () => {
|
|
118
|
-
pinBoard(host.org, '');
|
|
119
|
-
go(path({ kind: 'screen', screen: 'issues' }));
|
|
120
|
-
},
|
|
121
|
-
active: screen === 'issues',
|
|
122
|
-
},
|
|
123
|
-
{ id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
|
|
124
|
-
{
|
|
125
|
-
id: 'machines',
|
|
126
|
-
label: 'Machines',
|
|
127
|
-
icon: _jsx(Cpu, { size: 16 }),
|
|
128
|
-
onPress: () => go(path({ kind: 'settings', section: 'machines' })),
|
|
129
|
-
active: r.kind === 'settings' && r.section === 'machines',
|
|
130
|
-
},
|
|
131
|
-
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
|
|
132
|
-
];
|
|
133
|
-
return { links, more };
|
|
134
|
-
}
|
|
135
95
|
/** The wordmark: the product's name, as its host calls it, which leads to New. */
|
|
136
96
|
function Name({ onPress }) {
|
|
137
97
|
const { name } = useHost();
|
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;
|
package/lib/customize/agents.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 {
|
|
24
|
-
|
|
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 =
|
|
31
|
-
const offered =
|
|
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(
|
|
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(
|
|
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 =
|
|
73
|
-
const catalog =
|
|
74
|
-
const on =
|
|
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
|
|
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(
|
|
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;
|