@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.
- package/README.md +109 -55
- 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 +42 -4
- package/lib/api/github.js +79 -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 -24
- package/lib/api/work.js +132 -27
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +21 -54
- 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 +255 -92
- package/lib/host.d.ts +1 -1
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +79 -47
- 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 +45 -0
- package/lib/panel.js +115 -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.d.ts +16 -1
- package/lib/project.js +100 -32
- 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 +28 -4
- package/lib/route.js +48 -1
- 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 +23 -9
- 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/merge.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The projects an organization works on and their issues, merged.
|
|
3
|
+
*
|
|
4
|
+
* A project is a repository: granted by the org's GitHub connection, mirrored
|
|
5
|
+
* onto the forge, or made there. The same repository arrives from several
|
|
6
|
+
* sources and is one row. An issue is read from the forge and from the task
|
|
7
|
+
* service's mirror of GitHub; the same issue (the same link) is one row with
|
|
8
|
+
* every source that carried it named.
|
|
9
|
+
*
|
|
10
|
+
* Which project an issue is for is the issue's own repository — the GitHub
|
|
11
|
+
* address its mirror records, or the forge repository it is filed on — and
|
|
12
|
+
* never the board it sits on: a board's key is not a repository.
|
|
13
|
+
*
|
|
14
|
+
* Pure: every function here takes rows and answers rows, so the screens and the
|
|
15
|
+
* tests share one reading of what is the same, what belongs where, and what
|
|
16
|
+
* comes first.
|
|
17
|
+
*/
|
|
18
|
+
/** `owner/name` as one key, whatever case either source used. */
|
|
19
|
+
export const keyOf = (owner, name) => (owner && name ? `${owner}/${name}`.toLowerCase() : '');
|
|
20
|
+
const time = (iso) => {
|
|
21
|
+
const t = Date.parse(iso);
|
|
22
|
+
return Number.isFinite(t) ? t : 0;
|
|
23
|
+
};
|
|
24
|
+
const later = (a, b) => (time(b) > time(a) ? b : a);
|
|
25
|
+
/**
|
|
26
|
+
* An issue's link as one key: https only, no query or fragment, no trailing
|
|
27
|
+
* slash, lower case. The same issue read twice has the same key.
|
|
28
|
+
*/
|
|
29
|
+
export function canon(url) {
|
|
30
|
+
try {
|
|
31
|
+
const u = new URL(url);
|
|
32
|
+
if (u.protocol !== 'https:')
|
|
33
|
+
return '';
|
|
34
|
+
return `https://${u.host}${u.pathname.replace(/\/+$/, '')}`.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The repository and number a GitHub or forge issue link names, or null. */
|
|
41
|
+
export function parse(url) {
|
|
42
|
+
const m = canon(url).match(/^https:\/\/[^/]+\/([^/]+)\/([^/]+)\/(issues|pull|pulls)\/(\d+)$/);
|
|
43
|
+
return m ? { repo: `${m[1]}/${m[2]}`, number: Number(m[4]), pull: m[3] !== 'issues' } : null;
|
|
44
|
+
}
|
|
45
|
+
/** Whether every word of `q` is somewhere in `text`. */
|
|
46
|
+
export function matches(q, ...text) {
|
|
47
|
+
const words = q.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
48
|
+
if (!words.length)
|
|
49
|
+
return true;
|
|
50
|
+
const hay = text.join(' ').toLowerCase();
|
|
51
|
+
return words.every((w) => hay.includes(w));
|
|
52
|
+
}
|
|
53
|
+
/** The account and repository an https clone address names, and whether the host is GitHub. */
|
|
54
|
+
export function addressOf(url) {
|
|
55
|
+
try {
|
|
56
|
+
const u = new URL(url.trim());
|
|
57
|
+
if (u.protocol !== 'https:')
|
|
58
|
+
return null;
|
|
59
|
+
const parts = u.pathname.replace(/\.git$/, '').split('/').filter(Boolean);
|
|
60
|
+
if (parts.length < 2)
|
|
61
|
+
return null;
|
|
62
|
+
const github = u.hostname.toLowerCase() === 'github.com';
|
|
63
|
+
if (github && parts.length !== 2)
|
|
64
|
+
return null;
|
|
65
|
+
return { owner: parts[parts.length - 2], name: parts[parts.length - 1], github };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const blank = (owner, name) => ({
|
|
72
|
+
key: keyOf(owner, name),
|
|
73
|
+
owner,
|
|
74
|
+
name,
|
|
75
|
+
description: '',
|
|
76
|
+
private: null,
|
|
77
|
+
branch: '',
|
|
78
|
+
activity: '',
|
|
79
|
+
issues: null,
|
|
80
|
+
pulls: null,
|
|
81
|
+
linked: false,
|
|
82
|
+
mirrored: false,
|
|
83
|
+
forge: '',
|
|
84
|
+
synced: '',
|
|
85
|
+
status: '',
|
|
86
|
+
slug: '',
|
|
87
|
+
codebase: '',
|
|
88
|
+
url: '',
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* The project an issue is for, or null. Its GitHub address first, then the
|
|
92
|
+
* forge repository it is filed on, then what an index row says — matched
|
|
93
|
+
* against the org's own projects, an `owner/name` exactly, a bare name only when
|
|
94
|
+
* one project answers to it. A board's key is never consulted.
|
|
95
|
+
*/
|
|
96
|
+
export function place(i, list) {
|
|
97
|
+
if (i.repo)
|
|
98
|
+
return list.find((p) => p.key === i.repo.toLowerCase()) ?? null;
|
|
99
|
+
if (i.forge) {
|
|
100
|
+
const f = i.forge.toLowerCase();
|
|
101
|
+
return list.find((p) => p.forge.toLowerCase() === f) ?? list.find((p) => p.codebase === f) ?? null;
|
|
102
|
+
}
|
|
103
|
+
const hint = i.hint
|
|
104
|
+
.trim()
|
|
105
|
+
.toLowerCase()
|
|
106
|
+
.replace(/^(https?:\/\/)?github\.com\//, '')
|
|
107
|
+
.replace(/\.git$/, '');
|
|
108
|
+
if (!hint)
|
|
109
|
+
return null;
|
|
110
|
+
if (hint.includes('/'))
|
|
111
|
+
return list.find((p) => p.key === hint) ?? null;
|
|
112
|
+
const named = list.filter((p) => p.forge.toLowerCase() === hint || p.codebase === hint || p.name.toLowerCase() === hint);
|
|
113
|
+
return named.length === 1 ? named[0] : null;
|
|
114
|
+
}
|
|
115
|
+
/** Whether a clone address is the forge's own. */
|
|
116
|
+
export function forgeHost(url) {
|
|
117
|
+
try {
|
|
118
|
+
const u = new URL(url);
|
|
119
|
+
return u.protocol === 'https:' && (u.hostname.toLowerCase() === 'git.hanzo.ai' || u.pathname.startsWith('/v1/git/'));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Every project the org works on, one row per repository: the repositories it
|
|
127
|
+
* linked from GitHub, GitHub's grants, the forge's repositories and the ones its
|
|
128
|
+
* issues are filed on, and the org's projects that build from one — each with
|
|
129
|
+
* its open work counted from `work` (null counts when the work was not read).
|
|
130
|
+
*
|
|
131
|
+
* A forge repository is the GitHub one it copies when a link targets it, when
|
|
132
|
+
* GitHub names it as that repository's `codebase`, or when it sits in a forge
|
|
133
|
+
* namespace named like the GitHub owner (an issue's link says which namespace).
|
|
134
|
+
*/
|
|
135
|
+
export function assemble({ granted = [], linked = [], held = [], sites = [], work = null }) {
|
|
136
|
+
const out = new Map();
|
|
137
|
+
// Each project by its forge name and by its codebase name, kept as it is put, so a forge row finds its own in one look.
|
|
138
|
+
const byForge = new Map();
|
|
139
|
+
const byCodebase = new Map();
|
|
140
|
+
const put = (p) => {
|
|
141
|
+
if (!p.key)
|
|
142
|
+
return;
|
|
143
|
+
out.set(p.key, p);
|
|
144
|
+
if (p.forge)
|
|
145
|
+
byForge.set(p.forge.toLowerCase(), p.key);
|
|
146
|
+
if (p.codebase)
|
|
147
|
+
byCodebase.set(p.codebase, p.key);
|
|
148
|
+
};
|
|
149
|
+
const at = (owner, name) => out.get(keyOf(owner, name)) ?? blank(owner, name);
|
|
150
|
+
for (const l of linked) {
|
|
151
|
+
const p = at(l.owner, l.name);
|
|
152
|
+
put({
|
|
153
|
+
...p,
|
|
154
|
+
branch: p.branch || l.branch,
|
|
155
|
+
activity: later(p.activity, l.synced),
|
|
156
|
+
linked: true,
|
|
157
|
+
mirrored: p.mirrored || (l.forge !== '' && l.status !== 'pending'),
|
|
158
|
+
forge: p.forge || (l.status === 'pending' ? '' : l.forge),
|
|
159
|
+
synced: later(p.synced, l.synced),
|
|
160
|
+
// A link whose forge copy could not be read says only that it is linked.
|
|
161
|
+
status: l.status || 'linked',
|
|
162
|
+
url: `https://github.com/${l.owner}/${l.name}`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const g of granted) {
|
|
166
|
+
const p = at(g.owner, g.name);
|
|
167
|
+
put({
|
|
168
|
+
...p,
|
|
169
|
+
private: g.private,
|
|
170
|
+
branch: p.branch || g.default_branch,
|
|
171
|
+
activity: later(p.activity, g.pushed_at),
|
|
172
|
+
linked: true,
|
|
173
|
+
codebase: g.codebase.toLowerCase(),
|
|
174
|
+
url: `https://github.com/${g.owner}/${g.name}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const all = () => [...out.values()];
|
|
178
|
+
// Forge namespaces, as the forge's own issue links spell them.
|
|
179
|
+
const spaces = new Set();
|
|
180
|
+
for (const i of work ?? []) {
|
|
181
|
+
const where = i.forge ? parse(i.url) : null;
|
|
182
|
+
if (where)
|
|
183
|
+
spaces.add(where.repo.split('/')[0].toLowerCase());
|
|
184
|
+
}
|
|
185
|
+
const named = (key) => (key ? out.get(key) : undefined);
|
|
186
|
+
const onForge = (name, owner) => {
|
|
187
|
+
const n = name.toLowerCase();
|
|
188
|
+
return (named(byForge.get(n)) ??
|
|
189
|
+
named(byCodebase.get(n)) ??
|
|
190
|
+
[...spaces].map((space) => out.get(keyOf(space, name))).find((p) => p !== undefined) ??
|
|
191
|
+
at(owner, name));
|
|
192
|
+
};
|
|
193
|
+
for (const h of held) {
|
|
194
|
+
const p = onForge(h.name, h.org);
|
|
195
|
+
put({
|
|
196
|
+
...p,
|
|
197
|
+
description: p.description || h.description,
|
|
198
|
+
// A linked repository's visibility is GitHub's own, never its forge copy's: unknown until GitHub says.
|
|
199
|
+
private: p.linked ? p.private : (p.private ?? !h.public),
|
|
200
|
+
branch: p.branch || h.branch,
|
|
201
|
+
activity: later(p.activity, h.updated),
|
|
202
|
+
mirrored: true,
|
|
203
|
+
// A link's copy keeps its own name: a forge repository of the same address is not it.
|
|
204
|
+
forge: p.forge || h.name,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
for (const i of work ?? []) {
|
|
208
|
+
if (!i.forge)
|
|
209
|
+
continue;
|
|
210
|
+
const where = parse(i.url);
|
|
211
|
+
const p = onForge(i.forge, where ? where.repo.split('/')[0] : '');
|
|
212
|
+
put({ ...p, mirrored: true, forge: p.forge || i.forge });
|
|
213
|
+
}
|
|
214
|
+
for (const site of sites) {
|
|
215
|
+
const where = addressOf(site.repo);
|
|
216
|
+
// GitHub's, or the forge's own (git.hanzo.ai, or the platform's /v1/git); a site built from anywhere else is not a project here.
|
|
217
|
+
if (!where || (!where.github && !forgeHost(site.repo)))
|
|
218
|
+
continue;
|
|
219
|
+
const p = where.github ? at(where.owner, where.name) : onForge(where.name, where.owner);
|
|
220
|
+
put({
|
|
221
|
+
...p,
|
|
222
|
+
branch: p.branch || site.branch,
|
|
223
|
+
activity: later(p.activity, site.updated),
|
|
224
|
+
linked: p.linked || where.github,
|
|
225
|
+
mirrored: p.mirrored || !where.github,
|
|
226
|
+
forge: p.forge || (where.github ? '' : where.name),
|
|
227
|
+
slug: p.slug || site.slug,
|
|
228
|
+
url: p.url || (where.github ? `https://github.com/${where.owner}/${where.name}` : ''),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (work) {
|
|
232
|
+
const counted = new Map();
|
|
233
|
+
const list = all();
|
|
234
|
+
for (const i of work) {
|
|
235
|
+
const p = place(i, list);
|
|
236
|
+
if (!p)
|
|
237
|
+
continue;
|
|
238
|
+
const c = counted.get(p.key) ?? { issues: 0, pulls: 0, last: '' };
|
|
239
|
+
if (i.state === 'open' && i.pull)
|
|
240
|
+
c.pulls++;
|
|
241
|
+
else if (i.state === 'open')
|
|
242
|
+
c.issues++;
|
|
243
|
+
c.last = later(c.last, i.updated);
|
|
244
|
+
counted.set(p.key, c);
|
|
245
|
+
}
|
|
246
|
+
for (const p of list) {
|
|
247
|
+
const c = counted.get(p.key);
|
|
248
|
+
put({ ...p, issues: c?.issues ?? 0, pulls: c?.pulls ?? 0, activity: later(p.activity, c?.last ?? '') });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return all();
|
|
252
|
+
}
|
|
253
|
+
/** Each issue with the project it is for named as its `repo`, where one answers. */
|
|
254
|
+
export function attach(list, projects) {
|
|
255
|
+
return list.map((i) => {
|
|
256
|
+
const p = place(i, projects);
|
|
257
|
+
return p ? { ...i, repo: `${p.owner}/${p.name}` } : i;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** Most recent activity first; the unknown last; the address breaks a tie, so the order never shuffles. */
|
|
261
|
+
export function byActivity(a, b) {
|
|
262
|
+
return time(b.activity) - time(a.activity) || a.key.localeCompare(b.key);
|
|
263
|
+
}
|
|
264
|
+
export function pickProjects(list, f = {}) {
|
|
265
|
+
return list
|
|
266
|
+
.filter((p) => !f.owner || p.owner.toLowerCase() === f.owner.toLowerCase())
|
|
267
|
+
.filter((p) => !f.open || (p.issues ?? 0) + (p.pulls ?? 0) > 0)
|
|
268
|
+
.filter((p) => matches(f.q ?? '', `${p.owner}/${p.name}`, p.description))
|
|
269
|
+
.sort(byActivity);
|
|
270
|
+
}
|
|
271
|
+
/** The owners named, once each whatever the case, in name order. */
|
|
272
|
+
export function owners(...names) {
|
|
273
|
+
const seen = new Map();
|
|
274
|
+
for (const n of names.flat())
|
|
275
|
+
if (n && !seen.has(n.toLowerCase()))
|
|
276
|
+
seen.set(n.toLowerCase(), n);
|
|
277
|
+
return [...seen.values()].sort((a, b) => a.localeCompare(b));
|
|
278
|
+
}
|
|
279
|
+
const union = (a, b) => {
|
|
280
|
+
const out = [...a];
|
|
281
|
+
const have = new Set(a.map((x) => x.toLowerCase()));
|
|
282
|
+
for (const x of b) {
|
|
283
|
+
if (have.has(x.toLowerCase()))
|
|
284
|
+
continue;
|
|
285
|
+
have.add(x.toLowerCase());
|
|
286
|
+
out.push(x);
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* One row per issue. The same issue read from several sources is the row the
|
|
292
|
+
* most recently updated one describes, with every source named and every label
|
|
293
|
+
* and assignee any of them carries.
|
|
294
|
+
*/
|
|
295
|
+
export function issues(...lists) {
|
|
296
|
+
const out = new Map();
|
|
297
|
+
for (const i of lists.flat()) {
|
|
298
|
+
if (!i.key)
|
|
299
|
+
continue;
|
|
300
|
+
const had = out.get(i.key);
|
|
301
|
+
if (!had) {
|
|
302
|
+
out.set(i.key, { ...i, sources: [...new Set(i.sources)] });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const [lead, rest] = time(i.updated) > time(had.updated) ? [i, had] : [had, i];
|
|
306
|
+
out.set(i.key, {
|
|
307
|
+
...lead,
|
|
308
|
+
repo: lead.repo || rest.repo,
|
|
309
|
+
forge: lead.forge || rest.forge,
|
|
310
|
+
hint: lead.hint || rest.hint,
|
|
311
|
+
number: lead.number || rest.number,
|
|
312
|
+
title: lead.title || rest.title,
|
|
313
|
+
body: lead.body || rest.body,
|
|
314
|
+
url: lead.url || rest.url,
|
|
315
|
+
board: lead.board || rest.board,
|
|
316
|
+
labels: union(lead.labels, rest.labels),
|
|
317
|
+
assignees: union(lead.assignees, rest.assignees),
|
|
318
|
+
sources: [...new Set([...had.sources, ...i.sources])],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return [...out.values()];
|
|
322
|
+
}
|
|
323
|
+
/** Every label and assignee the list carries, for its filters, in name order. */
|
|
324
|
+
export function facets(list) {
|
|
325
|
+
const order = (a, b) => a.localeCompare(b);
|
|
326
|
+
return {
|
|
327
|
+
labels: list.reduce((acc, i) => union(acc, i.labels), []).sort(order),
|
|
328
|
+
assignees: list.reduce((acc, i) => union(acc, i.assignees), []).sort(order),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Where a run on issue `i` works, on project `p`: where the issue lives, so the
|
|
333
|
+
* run's pull request is opened beside it. A GitHub issue's run works on GitHub
|
|
334
|
+
* (a linked project's forge copy is the forge's own repository, and a pull
|
|
335
|
+
* request there would not reach GitHub's issue); a forge issue's on the forge.
|
|
336
|
+
* Any other issue (a Linear or a board's row) works wherever the project can be
|
|
337
|
+
* worked on: its forge copy, else GitHub. Null when the project is neither.
|
|
338
|
+
*/
|
|
339
|
+
export function home(i, p) {
|
|
340
|
+
if (i.sources.includes('github') && p.linked)
|
|
341
|
+
return 'github';
|
|
342
|
+
if (i.sources.includes('forge') && p.forge)
|
|
343
|
+
return 'forge';
|
|
344
|
+
return p.forge ? 'forge' : p.linked ? 'github' : null;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The number a run on `p` closes for issue `i`, or 0: the issue's own number,
|
|
348
|
+
* when the run works where the issue lives. A board's or Linear's number closes
|
|
349
|
+
* nothing, and neither does a GitHub issue run on the forge, or the reverse.
|
|
350
|
+
*/
|
|
351
|
+
export function closes(i, p) {
|
|
352
|
+
if (i.number <= 0)
|
|
353
|
+
return 0;
|
|
354
|
+
const at = home(i, p);
|
|
355
|
+
if (at === 'github' && i.sources.includes('github'))
|
|
356
|
+
return i.number;
|
|
357
|
+
if (at === 'forge' && i.sources.includes('forge'))
|
|
358
|
+
return i.number;
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
/** How a person refers to an issue: `#N` where N is the repository's own number, else the board's handle. */
|
|
362
|
+
export function handle(i) {
|
|
363
|
+
if (!i.number)
|
|
364
|
+
return '';
|
|
365
|
+
if (i.sources.includes('github') || i.sources.includes('forge'))
|
|
366
|
+
return `#${i.number}`;
|
|
367
|
+
return i.board ? `${i.board}-${i.number}` : '';
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* What New's composer holds for an issue: its title and handle, its link, and
|
|
371
|
+
* its body — the ask a person would write. `#N` only when the run closes N; any
|
|
372
|
+
* other number is the board's (`LINEAR-5`), so the agent cannot mistake it for
|
|
373
|
+
* an issue on the repository it works on.
|
|
374
|
+
*/
|
|
375
|
+
export function ask(i, closing = 0) {
|
|
376
|
+
const tag = closing ? `#${closing}` : i.board && i.number ? `${i.board}-${i.number}` : '';
|
|
377
|
+
const head = tag ? `${i.title} (${tag})` : i.title;
|
|
378
|
+
const body = i.body.trim();
|
|
379
|
+
return [head, i.url, body ? `\n${body}` : ''].filter(Boolean).join('\n');
|
|
380
|
+
}
|
package/lib/meter.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Read } from './data.ts';
|
|
2
|
+
import { type Standing } from './standing.ts';
|
|
3
|
+
import { type Label } from './plan.ts';
|
|
4
|
+
/** The ink a share is said in: quiet until it is close, and the one red when it is used. */
|
|
5
|
+
export declare const INK: {
|
|
6
|
+
readonly ok: '$soft';
|
|
7
|
+
readonly near: '$yellow10';
|
|
8
|
+
readonly limited: '$red10';
|
|
9
|
+
};
|
|
10
|
+
/** A plan's name with its rung beside it, small: `Max` `20x`. */
|
|
11
|
+
export declare function Title({ label, size }: {
|
|
12
|
+
label: Label;
|
|
13
|
+
size?: '$3' | '$5' | '$6';
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/** The standing, drawn: what `Meter` shows once it has read. */
|
|
16
|
+
export declare function Reading({ s, onPlans, onBilling, now }: {
|
|
17
|
+
s: Standing;
|
|
18
|
+
onPlans: () => void;
|
|
19
|
+
onBilling: () => void;
|
|
20
|
+
now?: number;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* The block an account menu leads with: `read` is `useStanding`, asked while
|
|
24
|
+
* the menu is open. `onPlans` opens the plans and `onBilling` the page credits
|
|
25
|
+
* live on; the host says where each is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function Meter({ read, onPlans, onBilling }: {
|
|
28
|
+
read: Read<Standing | null>;
|
|
29
|
+
onPlans: () => void;
|
|
30
|
+
onBilling: () => void;
|
|
31
|
+
}): import("react").JSX.Element;
|
package/lib/meter.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The plan, at the top of an account menu: what the account is on, and its
|
|
4
|
+
* meter. One block for every account menu — Dev's, the app's column, Chat's —
|
|
5
|
+
* so the plan reads the same wherever the menu opens.
|
|
6
|
+
*
|
|
7
|
+
* Max 20x Session 40% · Resets 5:00 PM / Today 12% / Month 62%
|
|
8
|
+
* Free 62% of today's free usage left · Upgrade
|
|
9
|
+
* Pay as you go $42.10 in credits · Add funds
|
|
10
|
+
*
|
|
11
|
+
* A plan's meter is its windows as shares and never money: credits are
|
|
12
|
+
* Billing's. Only an account with no plan and money in its balance sees the
|
|
13
|
+
* balance, because that balance is what it spends.
|
|
14
|
+
*/
|
|
15
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
16
|
+
import { money } from './api/billing.js';
|
|
17
|
+
import { left, rows } from './standing.js';
|
|
18
|
+
import { when } from './plan.js';
|
|
19
|
+
/** The ink a share is said in: quiet until it is close, and the one red when it is used. */
|
|
20
|
+
export const INK = { ok: '$soft', near: '$yellow10', limited: '$red10' };
|
|
21
|
+
/** A plan's name with its rung beside it, small: `Max` `20x`. */
|
|
22
|
+
export function Title({ label, size = '$3' }) {
|
|
23
|
+
return (_jsxs(XStack, { items: "baseline", gap: "$1.5", minW: 0, "data-slot": "plan-name", children: [_jsx(SizableText, { size: size, color: "$ink", style: { fontWeight: 600 }, numberOfLines: 1, children: label.name }), label.tag ? (_jsx(SizableText, { size: "$1", color: "$soft", px: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", "data-slot": "plan-tag", children: label.tag })) : null] }));
|
|
24
|
+
}
|
|
25
|
+
/** A quiet text button. */
|
|
26
|
+
function Act({ label, onPress }) {
|
|
27
|
+
return (_jsx(XStack, { render: "button", "aria-label": label, onPress: onPress, px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", shrink: 0, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: label }) }));
|
|
28
|
+
}
|
|
29
|
+
/** The standing, drawn: what `Meter` shows once it has read. */
|
|
30
|
+
export function Reading({ s, onPlans, onBilling, now }) {
|
|
31
|
+
const windows = s.kind === 'plan' ? rows(s.limits, now) : [];
|
|
32
|
+
const share = s.kind === 'free' ? left(s.allowance) : null;
|
|
33
|
+
const resets = s.allowance ? when(s.allowance.resets, now) : '';
|
|
34
|
+
return (_jsxs(YStack, { px: "$2", py: "$1.5", gap: "$1", "data-slot": "meter", "data-kind": s.kind, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(XStack, { flex: 1, minW: 0, children: s.label ? _jsx(Title, { label: s.label }) : null }), s.kind === 'free' ? _jsx(Act, { label: "Upgrade", onPress: onPlans }) : s.kind === 'credits' ? _jsx(Act, { label: "Add funds", onPress: onBilling }) : null] }), windows.map((w) => (_jsxs(XStack, { items: "baseline", gap: "$2", "data-slot": "meter-window", "data-state": w.state, children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 56, shrink: 0, children: w.name }), _jsxs(SizableText, { size: "$1", color: INK[w.state], style: { fontVariantNumeric: 'tabular-nums' }, shrink: 0, children: [w.percent, "%"] }), _jsx(SizableText, { size: "$1", color: "$soft", flex: 1, minW: 0, numberOfLines: 1, text: "right", children: w.resets })] }, w.name))), s.kind === 'free' && share !== null ? (_jsx(SizableText, { size: "$1", color: share === 0 ? '$red10' : '$soft', "data-slot": "meter-free", children: `${share}% of today’s free usage left${resets ? ` · Resets ${resets}` : ''}` })) : null, s.kind === 'credits' && s.balance !== null ? (_jsx(SizableText, { size: "$1", color: "$soft", "data-slot": "meter-balance", children: `${money(s.balance)} in credits` })) : null] }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The block an account menu leads with: `read` is `useStanding`, asked while
|
|
38
|
+
* the menu is open. `onPlans` opens the plans and `onBilling` the page credits
|
|
39
|
+
* live on; the host says where each is.
|
|
40
|
+
*/
|
|
41
|
+
export function Meter({ read, onPlans, onBilling }) {
|
|
42
|
+
if (read.value)
|
|
43
|
+
return _jsx(Reading, { s: read.value, onPlans: onPlans, onBilling: onBilling });
|
|
44
|
+
return (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1.5", "data-slot": "meter", children: read.error ? 'Your plan could not be read.' : 'Reading your plan…' }));
|
|
45
|
+
}
|
package/lib/mind.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Target } from './api/call.ts';
|
|
2
|
+
import { type Model } from './api/models.ts';
|
|
3
|
+
export type Effort = 'low' | 'medium' | 'high';
|
|
4
|
+
export declare const EFFORTS: readonly {
|
|
5
|
+
id: Effort;
|
|
6
|
+
label: string;
|
|
7
|
+
note: string;
|
|
8
|
+
}[];
|
|
9
|
+
/** What a person chose: the model (Enso routes) and the effort. */
|
|
10
|
+
export interface Choice {
|
|
11
|
+
model: string;
|
|
12
|
+
effort: Effort;
|
|
13
|
+
}
|
|
14
|
+
export declare const FIRST: Choice;
|
|
15
|
+
/** The kept choice, Enso and Medium where nothing is kept. Stable while the stored text is. */
|
|
16
|
+
export declare function kept(): Choice;
|
|
17
|
+
/** Keep a change, in this browser, for every surface that reads it. */
|
|
18
|
+
export declare function keep(patch: Partial<Choice>): void;
|
|
19
|
+
/** The kept choice and the way to change it. The server and the first render read the default. */
|
|
20
|
+
export declare function useChoice(): readonly [Choice, (patch: Partial<Choice>) => void];
|
|
21
|
+
/** Whether `model` takes an effort: the router and an undescribed model do, a listed one only if it reasons. */
|
|
22
|
+
export declare function reasons(model: string, catalog: readonly Model[]): boolean;
|
|
23
|
+
/** A model's name as the catalog spells it; Enso for the router. */
|
|
24
|
+
export declare function nameOf(model: string, catalog: readonly Model[]): string;
|
|
25
|
+
/** `GET /v1/models` for a target, asked once and shared; a refusal is not kept, so the next ask goes again. */
|
|
26
|
+
export declare function catalog(t: Target): Promise<Model[]>;
|
|
27
|
+
/** The catalog as a chooser draws it, and the list a kept pick is judged against. */
|
|
28
|
+
export interface Catalog {
|
|
29
|
+
/** What the chooser offers: the list read, else the last one this browser kept, else Enso alone. */
|
|
30
|
+
models: Model[];
|
|
31
|
+
/** What was read, for a pick to stand on: empty when nothing was, so a kept pick stands. */
|
|
32
|
+
list: Model[];
|
|
33
|
+
loading: boolean;
|
|
34
|
+
/** Why there is no list to offer, in a reader's words; null when there is one. */
|
|
35
|
+
error: string | null;
|
|
36
|
+
/** A list shown from before, because this read failed. */
|
|
37
|
+
note: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The platform's catalog, as a hook: what was read already is there on the
|
|
41
|
+
* first render. The gateway answers 429 once the org's quota is spent, and a
|
|
42
|
+
* list that vanishes then leaves nothing to choose from, so a failed read shows
|
|
43
|
+
* the last list this browser kept, with a note; with none kept, Enso alone, and
|
|
44
|
+
* the reason in the chooser — never the platform's own words.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useCatalog(t: Target): Catalog;
|
|
47
|
+
/** The chip's whole state: the catalog, what answers next, and how to change either. */
|
|
48
|
+
export interface Mind {
|
|
49
|
+
model: string;
|
|
50
|
+
effort: Effort;
|
|
51
|
+
models: Model[];
|
|
52
|
+
loading: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
/** A list shown from before, because this read failed. */
|
|
55
|
+
note: string;
|
|
56
|
+
/** A premium pick holds for this screen; any other is kept. */
|
|
57
|
+
pick: (id: string) => void;
|
|
58
|
+
pace: (effort: Effort) => void;
|
|
59
|
+
}
|
|
60
|
+
export declare function useMind(t: Target): Mind;
|