@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.
Files changed (96) hide show
  1. package/README.md +103 -49
  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 +24 -4
  9. package/lib/api/github.js +60 -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 -38
  19. package/lib/api/work.js +132 -44
  20. package/lib/builder.d.ts +0 -10
  21. package/lib/builder.js +12 -52
  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 +139 -71
  48. package/lib/index.d.ts +28 -2
  49. package/lib/index.js +26 -2
  50. package/lib/landing.d.ts +0 -10
  51. package/lib/landing.js +70 -38
  52. package/lib/merge.d.ts +223 -0
  53. package/lib/merge.js +380 -0
  54. package/lib/meter.d.ts +31 -0
  55. package/lib/meter.js +45 -0
  56. package/lib/mind.d.ts +60 -0
  57. package/lib/mind.js +187 -0
  58. package/lib/nav.d.ts +11 -0
  59. package/lib/nav.js +75 -0
  60. package/lib/panel.d.ts +46 -0
  61. package/lib/panel.js +116 -0
  62. package/lib/pick.d.ts +8 -0
  63. package/lib/pick.js +18 -1
  64. package/lib/plan.d.ts +78 -0
  65. package/lib/plan.js +133 -0
  66. package/lib/project.js +16 -6
  67. package/lib/prompt.d.ts +75 -0
  68. package/lib/prompt.js +125 -0
  69. package/lib/prose.js +14 -10
  70. package/lib/reply.d.ts +36 -0
  71. package/lib/reply.js +124 -0
  72. package/lib/route.d.ts +16 -0
  73. package/lib/route.js +40 -0
  74. package/lib/run.js +87 -52
  75. package/lib/section.d.ts +1 -1
  76. package/lib/section.js +7 -11
  77. package/lib/settings/billing.js +16 -9
  78. package/lib/settings/code.js +15 -12
  79. package/lib/settings/credits.d.ts +9 -0
  80. package/lib/settings/credits.js +110 -0
  81. package/lib/settings/plan.d.ts +17 -0
  82. package/lib/settings/plan.js +108 -0
  83. package/lib/settings/usage.js +11 -119
  84. package/lib/shelf.js +3 -3
  85. package/lib/standing.d.ts +44 -0
  86. package/lib/standing.js +64 -0
  87. package/lib/sync.js +22 -8
  88. package/lib/tabs.d.ts +53 -0
  89. package/lib/tabs.js +197 -0
  90. package/lib/tools.d.ts +2 -9
  91. package/lib/tools.js +20 -25
  92. package/lib/transcript.d.ts +10 -1
  93. package/lib/transcript.js +25 -10
  94. package/lib/voice.d.ts +2 -13
  95. package/lib/voice.js +7 -73
  96. package/package.json +20 -3
package/lib/landing.js CHANGED
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * New: the empty state. A heading at the top of the column, and at the foot of
4
4
  * the pane the composer — where the run goes (Cloud: the sandbox with the
5
5
  * codebase's environment; or one of the org's machines, under remote control),
6
- * which repository and branch it starts from, and the ask itself.
7
- * Under it: attach, dictate, the mode, and the model and effort on the right.
6
+ * which repository and branch it starts from, and the ask itself — the one
7
+ * composer Chat draws too (prompt.tsx): attach and the mode at its start, the
8
+ * model and effort, dictation and send at its end.
8
9
  *
9
10
  * Sending starts a coding run on a project and opens it. A project is a
10
11
  * repository — the org's on the forge, or one its GitHub grants — so a run
@@ -16,39 +17,36 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
16
17
  import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
17
18
  import { Button } from '@hanzo/ui';
18
19
  import { ModeSelect } from '@hanzo/ui/agents';
19
- import { Composer } from '@hanzo/ui/chat';
20
- import { BranchSelect, ChipSelect, FooterLink, HanzoMark, RepoSelect } from '@hanzo/ui/product';
21
- import { ModelPicker } from '@hanzo/ui/models';
20
+ import { BranchSelect, ChipSelect, FooterLink, RepoSelect } from '@hanzo/ui/product';
22
21
  import { useLimits } from '@hanzo/ui/product/useLimits';
22
+ import { useDictation, useVoice } from '@hanzo/voice';
23
23
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
24
24
  import { start, unhonoured } from './api/coding.js';
25
25
  import { asRepo, chosen, codebases, one } from './api/codebases.js';
26
26
  import { repos as githubRepos } from './api/github.js';
27
- import { read, SETUP } from './api/environment.js';
27
+ import { plain, read, SETUP, unset } from './api/environment.js';
28
+ import { homes as readHomes } from './api/work.js';
28
29
  import { SetupDialog } from './environment.js';
29
- import { ENSO, limits as readLimits, models } from './api/models.js';
30
- import { usePick } from './pick.js';
30
+ import { limits as readLimits } from './api/models.js';
31
31
  import { ready, SANDBOX } from './api/places.js';
32
- import { asHub, isForge, isHub } from './choice.js';
33
- import { useKept, usePlaces, useRead } from './data.js';
32
+ import { asHub, isForge, isHub, settle } from './choice.js';
33
+ import { useCached, useKept, usePlaces, useRead } from './data.js';
34
34
  import { useHost, useTarget } from './host.js';
35
- import { pane } from './pane.js';
35
+ import { useMind } from './mind.js';
36
36
  import { usePrefs } from './prefs.js';
37
37
  import { Publish } from './publish.js';
38
38
  import { path } from './route.js';
39
- import { Attach, compose, Dictate, Files } from './tools.js';
39
+ import { MEASURE, Prompt, useEar } from './prompt.js';
40
+ import { compose, Files, pick } from './tools.js';
41
+ import { useSpoken } from './voice.js';
40
42
  /** One vocabulary for the mode, on New and in a workspace. */
41
43
  export const MODES = [
42
44
  { id: 'build', label: 'Build', hint: 'Edits, commits and pushes a branch' },
43
45
  { id: 'plan', label: 'Plan', hint: 'Plans the change and writes nothing' },
44
46
  ];
45
- export const EFFORTS = [
46
- { id: 'low', label: 'Low' },
47
- { id: 'medium', label: 'Medium' },
48
- { id: 'high', label: 'High' },
49
- ];
50
- const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium', ask: '' };
51
- const COLUMN = 768;
47
+ /** The org's repositories before they have been read: no answer, so nothing is judged from it. */
48
+ const NOHOMES = { homes: [], whole: false };
49
+ const FIRST = { repo: null, branch: '', place: '', mode: 'build', ask: '', issue: 0 };
52
50
  const UNSENT = 'hanzo.build.unsent';
53
51
  let held = { draft: '', files: [] };
54
52
  function unsent() {
@@ -81,11 +79,15 @@ export function Landing({ onStarted }) {
81
79
  const signed = Boolean(host.person);
82
80
  const { prefs } = usePrefs();
83
81
  const [stored, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
84
- // Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
85
- const kept = stored ?? { ...FIRST, ...prefs.code };
82
+ // What was chosen here, over the person's coding defaults (Settings → Code), over
83
+ // the first run's. Field by field: a codebase or an issue handed over from its own
84
+ // screen keeps only the codebase and the words, and the rest still has a value.
85
+ const kept = settle(FIRST, prefs.code, stored);
86
86
  const set = (patch) => keep({ ...kept, ...patch });
87
87
  const [was] = useState(unsent);
88
88
  const [draft, setDraft] = useState(kept.ask || was.draft);
89
+ // The issue handed over with the words, while the run is still on its repository.
90
+ const [issue, setIssue] = useState(kept.ask ? kept.issue : 0);
89
91
  const [files, setFiles] = useState(was.files);
90
92
  useEffect(() => hold({ draft, files }), [draft, files]);
91
93
  const [busy, setBusy] = useState(false);
@@ -98,18 +100,27 @@ export function Landing({ onStarted }) {
98
100
  // A codebase or an issue hands its words over once. Left in the kept choice,
99
101
  // every later visit to New would put them back.
100
102
  useEffect(() => {
101
- if (kept.ask)
102
- set({ ask: '' });
103
+ if (kept.ask || kept.issue)
104
+ set({ ask: '', issue: 0 });
103
105
  // The handover is read on this mount only.
104
106
  // eslint-disable-next-line react-hooks/exhaustive-deps
105
107
  }, []);
106
108
  const places = usePlaces(t, signed);
107
- const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
109
+ // The model and effort Chat shares, and dictation into the ask.
110
+ const mind = useMind(t);
108
111
  const { limits } = useLimits(signed ? (signal) => readLimits(t, signal) : null, t);
109
- const [model, pickModel] = usePick(kept.model, (id) => set({ model: id }), catalog.value);
112
+ const voice = useVoice({ speech: useEar(t), language: useSpoken(), ...useDictation(draft, setDraft) });
110
113
  // The chosen codebase's environment, so New can say when it has none yet.
111
114
  const codebase = isForge(kept.repo) ? kept.repo.name : '';
112
- const env = useRead(signed && codebase ? () => read(t, codebase) : null, null, [t, signed, codebase]);
115
+ // The org's repositories on the forge: what a run and an environment can name.
116
+ const homes = useCached(signed ? `${host.org}:homes` : null, (signal) => readHomes(t, signal), NOHOMES);
117
+ // A kept codebase the org's whole list does not hold — a board's key once handed over as one — is not offered as one.
118
+ const stranger = codebase !== '' && homes.value.whole && !homes.value.homes.some((h) => h.name.toLowerCase() === codebase.toLowerCase());
119
+ // Read once the list has answered (or failed), so a name that is not the org's is never asked for.
120
+ const settled = homes.value !== NOHOMES || homes.error !== null;
121
+ const found = useRead(signed && codebase && settled && !stranger ? () => read(t, codebase) : null, null, [t, signed, codebase, settled, stranger]);
122
+ // A read kept from the codebase before is not this one's.
123
+ const env = { ...found, value: found.value && found.value.repo === codebase && !stranger ? found.value : null };
113
124
  const place = places.value.find((p) => p.id === kept.place) ?? SANDBOX;
114
125
  const known = useRef(new Map());
115
126
  const hubs = useRef(new Map());
@@ -130,9 +141,9 @@ export function Landing({ onStarted }) {
130
141
  // Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
131
142
  const home = kept.repo?.default_branch || 'main';
132
143
  const loadBranches = useCallback(async (q) => {
133
- const found = await one(t, codebase);
144
+ const got = await one(t, codebase);
134
145
  const needle = q.trim().toLowerCase();
135
- const names = (found?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
146
+ const names = (got?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
136
147
  return { branches: names.map((name) => ({ name })), next: null };
137
148
  }, [t, codebase, home]);
138
149
  // The composer sends only a draft with words in it, and not while a send is out.
@@ -149,6 +160,17 @@ export function Landing({ onStarted }) {
149
160
  setPicking(true);
150
161
  return;
151
162
  }
163
+ // A plan or a setup routed to a machine is refused here, in its own words, before it leaves the page.
164
+ const why = unhonoured(kept.mode, place.id);
165
+ if (why) {
166
+ setNote(why);
167
+ return;
168
+ }
169
+ if (stranger) {
170
+ setNote(`${codebase} is not one of this organization’s repositories. Choose one, or leave it empty to start a new project.`);
171
+ setPicking(true);
172
+ return;
173
+ }
152
174
  setBusy(true);
153
175
  setNote('');
154
176
  try {
@@ -160,15 +182,17 @@ export function Landing({ onStarted }) {
160
182
  base: repo ? kept.branch : hub ? kept.branch || hub.default_branch : undefined,
161
183
  targetId: place.id,
162
184
  mode: kept.mode,
163
- model: model === ENSO ? undefined : model,
164
- effort: kept.effort,
185
+ model: mind.model,
186
+ effort: mind.effort,
187
+ issue,
165
188
  });
166
189
  setDraft('');
167
190
  setFiles([]);
191
+ setIssue(0);
168
192
  onStarted(run.session);
169
193
  }
170
194
  catch (e) {
171
- setNote(e.message);
195
+ setNote(plain(e, repo?.name ?? hub?.full_name ?? 'a new project', 'run'));
172
196
  }
173
197
  finally {
174
198
  setBusy(false);
@@ -184,18 +208,26 @@ export function Landing({ onStarted }) {
184
208
  // Sent once, on the sign-in that it waited for.
185
209
  // eslint-disable-next-line react-hooks/exhaustive-deps
186
210
  }, [signed]);
187
- /** Start the agent that finds this codebase's environment, and open it. A refusal is the dialog's to say. */
188
- const setup = async () => {
189
- const run = await start(t, { prompt: SETUP, repo: codebase, mode: 'setup' });
211
+ /** Start the agent that finds a codebase's environment, and open it: its run is the progress. A refusal is the dialog's to say. */
212
+ const setup = async (repo) => {
213
+ const run = await start(t, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: mind.effort });
190
214
  onStarted(run.session);
191
215
  };
216
+ /** The codebase a dialog chose, chosen here too. */
217
+ const choose = (h) => {
218
+ const row = chosen({ owner: h.label.split('/')[0] ?? '', name: h.name, full_name: h.label, default_branch: h.branch });
219
+ setIssue(0);
220
+ set({ repo: row, branch: row.default_branch });
221
+ };
192
222
  // Cloud is the sandbox, set up by the chosen codebase's environment.
193
223
  const setUp = codebase && env.value
194
224
  ? env.value.state === 'ready'
195
225
  ? `${codebase} environment`
196
226
  : env.value.state === 'proposed'
197
227
  ? `${codebase} environment proposed`
198
- : `no ${codebase} environment yet`
228
+ : unset(env.value)
229
+ ? `no ${codebase} environment yet`
230
+ : `${codebase} environment, empty`
199
231
  : '';
200
232
  const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
201
233
  id: p.id || 'sandbox',
@@ -207,6 +239,7 @@ export function Landing({ onStarted }) {
207
239
  const branch = kept.branch || kept.repo?.default_branch || 'main';
208
240
  const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) || isHub(kept.repo) ? kept.repo : null, onChange: (r) => {
209
241
  const hub = r.github ? hubs.current.get((r.full_name ?? "").toLowerCase()) : undefined;
242
+ setIssue(0);
210
243
  if (hub) {
211
244
  set({ repo: hub, branch: hub.default_branch });
212
245
  return;
@@ -228,10 +261,9 @@ export function Landing({ onStarted }) {
228
261
  });
229
262
  },
230
263
  }, placeholder: "Choose a project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
231
- const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ModelPicker, { quiet: true, size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: model, onChange: pickModel, loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
232
- return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", px: "$4", pb: "$10", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, gap: "$5", children: [_jsxs(XStack, { role: "heading", "aria-level": 1, justify: "center", items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(HanzoMark, { size: 26 }), _jsx(SizableText, { size: "$8", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' })] }), _jsxs(YStack, { gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
264
+ return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$5", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' }), _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: `${codebase} is not one of this organization’s repositories.` }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: "Choose a repository" })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
233
265
  ? `A proposed environment for ${codebase} is waiting for review.`
234
- : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, ...pane, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
266
+ : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, onAttach: () => pick(files, setFiles, setNote), tools: _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), mind: mind, limits: limits, voice: voice })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, homes: homes, open: offering, onOpenChange: setOffering, onStart: setup, onRepo: choose, onSaved: () => {
235
267
  setOffering(false);
236
268
  env.reload();
237
269
  } })) : null] }));
package/lib/merge.d.ts ADDED
@@ -0,0 +1,223 @@
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
+ export type Source = 'github' | 'forge' | 'linear' | 'agent' | 'board';
19
+ export interface Project {
20
+ /** `owner/name`, lower case: the one address every source is matched on. */
21
+ key: string;
22
+ owner: string;
23
+ name: string;
24
+ description: string;
25
+ /** Null when no source said. */
26
+ private: boolean | null;
27
+ /** The default branch, or '' when no source said. */
28
+ branch: string;
29
+ /** RFC 3339: the latest push, update or issue any source reports, or ''. */
30
+ activity: string;
31
+ /** Open issues, or null when the work could not be read. */
32
+ issues: number | null;
33
+ /** Open pull requests on the forge, or null when the work could not be read. */
34
+ pulls: number | null;
35
+ /** Granted by the org's GitHub connection. */
36
+ linked: boolean;
37
+ /** On the forge: mirrored from GitHub, or made there. */
38
+ mirrored: boolean;
39
+ /** The forge's own name for it, which is what a run and an environment name; '' when it is not known to be there. */
40
+ forge: string;
41
+ /** RFC 3339: when its link last synced, or ''. */
42
+ synced: string;
43
+ /** Its link's state: `synced`, `conflict`, `pending`, `paused`, `linked` when its forge copy could not be read; '' with no link. */
44
+ status: string;
45
+ /** The org's project (GET /v1/projects) that builds from it, by slug, or ''. */
46
+ slug: string;
47
+ /** The name GitHub's listing says it takes on the forge once brought there, or ''. */
48
+ codebase: string;
49
+ /** Its page on GitHub, or ''. */
50
+ url: string;
51
+ }
52
+ export interface Issue {
53
+ /** What makes two rows the same issue: its link, else its handle on its board, else its id. */
54
+ key: string;
55
+ /** `owner/name` of the repository it is filed on, once known; '' until then. */
56
+ repo: string;
57
+ /** The forge repository a forge row is filed on, by name; ''. */
58
+ forge: string;
59
+ /** What an index row says its repository is, unchecked: a run's repo, or a Linear team's key. Resolved, never used raw. */
60
+ hint: string;
61
+ number: number;
62
+ title: string;
63
+ body: string;
64
+ state: 'open' | 'closed';
65
+ labels: string[];
66
+ assignees: string[];
67
+ /** The issue's own page, https only, or ''. */
68
+ url: string;
69
+ sources: Source[];
70
+ /** RFC 3339, or ''. */
71
+ updated: string;
72
+ /** RFC 3339: when it was opened, or ''. */
73
+ created: string;
74
+ /** What the task service files it as: `issue`, `pr`, `task`, `bug`, … */
75
+ kind: string;
76
+ /** The board's column for it as the task service names it: `backlog`, `todo`, `in_progress`, `done`, `canceled`, or its own word. */
77
+ status: string;
78
+ /** `none` unless the board says otherwise. */
79
+ priority: string;
80
+ /** A pull request, not an issue. */
81
+ pull: boolean;
82
+ /** The board it was read from. Shown, filtered on, never resolved as a repository. */
83
+ board: string;
84
+ }
85
+ /** `owner/name` as one key, whatever case either source used. */
86
+ export declare const keyOf: (owner: string, name: string) => string;
87
+ /**
88
+ * An issue's link as one key: https only, no query or fragment, no trailing
89
+ * slash, lower case. The same issue read twice has the same key.
90
+ */
91
+ export declare function canon(url: string): string;
92
+ /** The repository and number a GitHub or forge issue link names, or null. */
93
+ export declare function parse(url: string): {
94
+ repo: string;
95
+ number: number;
96
+ pull: boolean;
97
+ } | null;
98
+ /** Whether every word of `q` is somewhere in `text`. */
99
+ export declare function matches(q: string, ...text: string[]): boolean;
100
+ /** A repository GitHub's listing grants, in the fields a project takes from it. */
101
+ export interface Granted {
102
+ owner: string;
103
+ name: string;
104
+ private: boolean;
105
+ default_branch: string;
106
+ pushed_at: string;
107
+ codebase: string;
108
+ }
109
+ /** A repository the forge lists, in the fields a project takes from it. */
110
+ export interface Held {
111
+ org: string;
112
+ name: string;
113
+ description: string;
114
+ branch: string;
115
+ public: boolean;
116
+ updated: string;
117
+ }
118
+ /** A repository linked from GitHub (GET /v1/sync), in the fields a project takes from it. */
119
+ export interface Linked {
120
+ owner: string;
121
+ name: string;
122
+ forge: string;
123
+ synced: string;
124
+ status: string;
125
+ branch: string;
126
+ }
127
+ /** One of the org's projects (GET /v1/projects) that builds from a repository. */
128
+ export interface Site {
129
+ slug: string;
130
+ /** Its clone address. */
131
+ repo: string;
132
+ branch: string;
133
+ /** RFC 3339, or ''. */
134
+ updated: string;
135
+ }
136
+ /** The account and repository an https clone address names, and whether the host is GitHub. */
137
+ export declare function addressOf(url: string): {
138
+ owner: string;
139
+ name: string;
140
+ github: boolean;
141
+ } | null;
142
+ /**
143
+ * The project an issue is for, or null. Its GitHub address first, then the
144
+ * forge repository it is filed on, then what an index row says — matched
145
+ * against the org's own projects, an `owner/name` exactly, a bare name only when
146
+ * one project answers to it. A board's key is never consulted.
147
+ */
148
+ export declare function place(i: Pick<Issue, 'repo' | 'forge' | 'hint'>, list: Project[]): Project | null;
149
+ /** Whether a clone address is the forge's own. */
150
+ export declare function forgeHost(url: string): boolean;
151
+ /** What the org's projects are assembled from. */
152
+ export interface Parts {
153
+ /** GitHub's grants to the org's connection: empty for anyone it does not answer. */
154
+ granted?: Granted[];
155
+ /** The repositories linked from GitHub, each with its forge copy. */
156
+ linked?: Linked[];
157
+ /** The forge's repositories. */
158
+ held?: Held[];
159
+ /** The org's projects that build from a repository. */
160
+ sites?: Site[];
161
+ /** Every issue and pull request, or null when they were not read. */
162
+ work?: Issue[] | null;
163
+ }
164
+ /**
165
+ * Every project the org works on, one row per repository: the repositories it
166
+ * linked from GitHub, GitHub's grants, the forge's repositories and the ones its
167
+ * issues are filed on, and the org's projects that build from one — each with
168
+ * its open work counted from `work` (null counts when the work was not read).
169
+ *
170
+ * A forge repository is the GitHub one it copies when a link targets it, when
171
+ * GitHub names it as that repository's `codebase`, or when it sits in a forge
172
+ * namespace named like the GitHub owner (an issue's link says which namespace).
173
+ */
174
+ export declare function assemble({ granted, linked, held, sites, work }: Parts): Project[];
175
+ /** Each issue with the project it is for named as its `repo`, where one answers. */
176
+ export declare function attach(list: Issue[], projects: Project[]): Issue[];
177
+ /** Most recent activity first; the unknown last; the address breaks a tie, so the order never shuffles. */
178
+ export declare function byActivity(a: Project, b: Project): number;
179
+ export interface ProjectFilter {
180
+ q?: string;
181
+ /** One owner, or '' for every one. */
182
+ owner?: string;
183
+ /** Only those with an open issue or pull request. */
184
+ open?: boolean;
185
+ }
186
+ export declare function pickProjects(list: Project[], f?: ProjectFilter): Project[];
187
+ /** The owners named, once each whatever the case, in name order. */
188
+ export declare function owners(...names: string[][]): string[];
189
+ /**
190
+ * One row per issue. The same issue read from several sources is the row the
191
+ * most recently updated one describes, with every source named and every label
192
+ * and assignee any of them carries.
193
+ */
194
+ export declare function issues(...lists: Issue[][]): Issue[];
195
+ /** Every label and assignee the list carries, for its filters, in name order. */
196
+ export declare function facets(list: Issue[]): {
197
+ labels: string[];
198
+ assignees: string[];
199
+ };
200
+ /**
201
+ * Where a run on issue `i` works, on project `p`: where the issue lives, so the
202
+ * run's pull request is opened beside it. A GitHub issue's run works on GitHub
203
+ * (a linked project's forge copy is the forge's own repository, and a pull
204
+ * request there would not reach GitHub's issue); a forge issue's on the forge.
205
+ * Any other issue (a Linear or a board's row) works wherever the project can be
206
+ * worked on: its forge copy, else GitHub. Null when the project is neither.
207
+ */
208
+ export declare function home(i: Pick<Issue, 'sources'>, p: Pick<Project, 'linked' | 'forge'>): 'github' | 'forge' | null;
209
+ /**
210
+ * The number a run on `p` closes for issue `i`, or 0: the issue's own number,
211
+ * when the run works where the issue lives. A board's or Linear's number closes
212
+ * nothing, and neither does a GitHub issue run on the forge, or the reverse.
213
+ */
214
+ export declare function closes(i: Pick<Issue, 'sources' | 'number'>, p: Pick<Project, 'linked' | 'forge'>): number;
215
+ /** How a person refers to an issue: `#N` where N is the repository's own number, else the board's handle. */
216
+ export declare function handle(i: Pick<Issue, 'sources' | 'number' | 'board'>): string;
217
+ /**
218
+ * What New's composer holds for an issue: its title and handle, its link, and
219
+ * its body — the ask a person would write. `#N` only when the run closes N; any
220
+ * other number is the board's (`LINEAR-5`), so the agent cannot mistake it for
221
+ * an issue on the repository it works on.
222
+ */
223
+ export declare function ask(i: Pick<Issue, 'title' | 'number' | 'url' | 'body' | 'board' | 'sources'>, closing?: number): string;