@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
@@ -13,8 +13,9 @@ import { Plus, X } from '@hanzogui/lucide-icons-2';
13
13
  import { Button, Dialog, DialogContent, DialogTitle, Input, Textarea } from '@hanzo/ui';
14
14
  import { useEffect, useState } from 'react';
15
15
  import { start } from './api/coding.js';
16
- import { read, refuse, removeSecret, save, setSecret, SETUP } from './api/environment.js';
16
+ import { plain, read, refuse, removeSecret, save, setSecret, SETUP } from './api/environment.js';
17
17
  import { useRead } from './data.js';
18
+ import { matches } from './merge.js';
18
19
  import { useHost, useTarget } from './host.js';
19
20
  const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
20
21
  export function Environment({ repo, busy }) {
@@ -44,7 +45,7 @@ export function Environment({ repo, busy }) {
44
45
  return _jsx(Soft, { children: "This run has no codebase, so it has no environment." });
45
46
  }
46
47
  if (env.error) {
47
- return _jsx(Soft, { children: env.error.message });
48
+ return _jsx(Soft, { children: `${repo}’s environment could not be read right now.` });
48
49
  }
49
50
  if (!shown) {
50
51
  return _jsx(Soft, { children: "Reading the environment\u2026" });
@@ -63,7 +64,7 @@ export function Environment({ repo, busy }) {
63
64
  return true;
64
65
  }
65
66
  catch (e) {
66
- setNote(e.message);
67
+ setNote(plain(e, repo, 'save'));
67
68
  return false;
68
69
  }
69
70
  finally {
@@ -78,7 +79,7 @@ export function Environment({ repo, busy }) {
78
79
  host.go(run.session);
79
80
  }
80
81
  catch (e) {
81
- setNote(e.message);
82
+ setNote(plain(e, repo, 'start'));
82
83
  setWorking(false);
83
84
  }
84
85
  };
@@ -102,27 +103,66 @@ export function Environment({ repo, busy }) {
102
103
  /**
103
104
  * New's offer to set a codebase up. An agent onboards it, or an org admin saves
104
105
  * it empty and writes the scripts by hand beside the next run.
106
+ *
107
+ * The repository is chosen from the org's own (the forge's, and every linked
108
+ * repository's copy there), never typed: a name that is not one of them — a
109
+ * board's key handed over as a codebase — is said so and cannot be set up.
105
110
  */
106
- export function SetupDialog({ repo, open, onOpenChange, onStart, onSaved, }) {
111
+ export function SetupDialog({ repo, homes, open, onOpenChange, onStart, onSaved, onRepo, }) {
107
112
  const host = useHost();
108
113
  const t = useTarget();
109
- const [working, setWorking] = useState(false);
114
+ const [chosen, setChosen] = useState(repo);
115
+ const [q, setQ] = useState('');
116
+ const [working, setWorking] = useState('');
110
117
  const [note, setNote] = useState('');
111
- const run = async (what, done) => {
112
- setWorking(true);
118
+ const [last, setLast] = useState('');
119
+ useEffect(() => {
120
+ if (!open)
121
+ return;
122
+ setChosen(repo);
123
+ setQ('');
124
+ setNote('');
125
+ setLast('');
126
+ }, [open, repo]);
127
+ const list = homes.value.homes;
128
+ const home = list.find((h) => h.name.toLowerCase() === chosen.toLowerCase()) ?? null;
129
+ // A name the whole list does not hold is not the org's; with part of the list unread, the platform decides.
130
+ const stranger = Boolean(chosen) && !home && homes.value.whole && !homes.loading;
131
+ const ready = Boolean(chosen) && !stranger && !homes.loading;
132
+ const shown = list.filter((h) => matches(q, h.label, h.name)).slice(0, 6);
133
+ const run = async (what) => {
134
+ if (!ready)
135
+ return;
136
+ // The org's own spelling of the name, when the list holds it.
137
+ const repo = home?.name ?? chosen;
138
+ setWorking(what);
139
+ setLast(what);
113
140
  setNote('');
114
141
  try {
115
- await what();
116
- done();
142
+ if (what === 'save') {
143
+ await save(t, repo, { install: '', start: '' });
144
+ onSaved(repo);
145
+ }
146
+ else {
147
+ await onStart(repo);
148
+ onOpenChange(false);
149
+ }
117
150
  }
118
151
  catch (e) {
119
- setNote(e.message);
152
+ setNote(plain(e, repo, what));
120
153
  }
121
154
  finally {
122
- setWorking(false);
155
+ setWorking('');
123
156
  }
124
157
  };
125
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Set up an environment" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Repository" }), _jsx(XStack, { px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: repo }) })] }), _jsx(SizableText, { size: "$2", color: "$soft", children: "An agent onboards the codebase: it explores it, writes the install and start scripts, and names the secrets it needs. It takes several minutes. Interrupt it anytime, or take over in the terminal." }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "space-between", items: "center", children: [host.admin ? (_jsx(Button, { size: "sm", variant: "secondary", disabled: working, onPress: () => void run(() => save(t, repo, { install: '', start: '' }), onSaved), children: "Skip & save" })) : (_jsx(YStack, {})), _jsx(Button, { size: "sm", variant: "primary", disabled: working, onPress: () => void run(onStart, () => onOpenChange(false)), children: "Start agent" })] })] })] }) }));
158
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Set up an environment" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Repository" }), _jsx(XStack, { px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: stranger ? '$ink' : '$borderColor', items: "center", gap: "$2", "aria-label": "Chosen repository", children: _jsx(SizableText, { size: "$2", color: home || (chosen && !stranger) ? '$ink' : '$soft', numberOfLines: 1, flex: 1, minW: 0, children: home ? home.label : chosen || 'Choose a repository' }) }), stranger ? (_jsx(SizableText, { size: "$1", color: "$ink", role: "alert", children: `${chosen} is not one of this organization’s repositories. Choose one below.` })) : null, _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find a repository\u2026", "aria-label": "Find a repository" }), _jsx(YStack, { gap: 2, "aria-label": "Repositories", children: homes.loading && !list.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Reading the repositories\u2026" })) : homes.error && !list.length ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "The repository list could not be read right now." }), _jsx(Button, { size: "sm", variant: "outline", onPress: homes.reload, children: "Read again" })] })) : shown.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: list.length ? 'Nothing matches.' : 'This organization has no repositories on the forge yet.' })) : (shown.map((h) => {
159
+ const on = h.name.toLowerCase() === chosen.toLowerCase();
160
+ return (_jsx(XStack, { render: "button", "aria-pressed": on, "aria-label": `Choose ${h.label}`, onPress: () => {
161
+ setChosen(h.name);
162
+ setNote('');
163
+ onRepo(h);
164
+ }, px: "$2.5", py: "$1.5", rounded: "$2", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, children: h.label }) }, h.name));
165
+ })) })] }), _jsx(SizableText, { size: "$2", color: "$soft", children: "An agent onboards the codebase: it explores it, writes the install and start scripts, and names the secrets it needs. It takes several minutes. Interrupt it anytime, or take over in the terminal." }), note ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(SizableText, { size: "$1", color: "$ink", role: "status", flex: 1, minW: 200, children: note }), last ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working !== '', onPress: () => void run(last), children: "Try again" })) : null] })) : null, _jsxs(XStack, { gap: "$2", justify: "space-between", items: "center", children: [host.admin ? (_jsx(Button, { size: "sm", variant: "secondary", disabled: working !== '' || !ready, onPress: () => void run('save'), children: working === 'save' ? 'Saving…' : 'Skip & save' })) : (_jsx(YStack, {})), _jsx(Button, { size: "sm", variant: "primary", disabled: working !== '' || !ready, onPress: () => void run('start'), children: working === 'start' ? 'Starting the agent…' : 'Start agent' })] })] })] }) }));
126
166
  }
127
167
  function Script({ label, value, onChange, locked, hint }) {
128
168
  return (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: label }), _jsx(Textarea, { value: value, onChangeText: onChange, disabled: locked, placeholder: hint, "aria-label": label, rows: 3, style: mono })] }));
package/lib/foot.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * The rail's foot: the card that offers Hanzo in Slack, and the menu the account
4
- * row opens — who is signed in, the organization they act in, Settings, Usage,
5
- * the plans, help and logging out.
4
+ * row opens — who is signed in, the plan and its meter (`Meter`), the
5
+ * organization they act in, Settings, Usage, Billing, the plans, help and
6
+ * logging out.
6
7
  * Everything it opens is an address of this page, but help, which is the
7
8
  * documentation.
8
9
  */
9
10
  import { useCallback, useEffect, useRef, useState } from 'react';
10
11
  import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
11
- import { Gauge, LifeBuoy, LogOut, Settings, Slack as Mark, Sparkles, X } from '@hanzogui/lucide-icons-2';
12
+ import { CreditCard, Gauge, LifeBuoy, LogOut, Settings, Slack as Mark, Sparkles, X } from '@hanzogui/lucide-icons-2';
12
13
  import { MenuLabel, MenuRow, MenuRule } from '@hanzo/ui/product';
13
- import { useHost } from './host.js';
14
+ import { useHost, useTarget } from './host.js';
15
+ import { Meter } from './meter.js';
16
+ import { useStanding } from './standing.js';
14
17
  import { path } from './route.js';
15
18
  export const DOCS = 'https://docs.hanzo.ai/docs/dev';
16
19
  /** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
@@ -52,17 +55,18 @@ export function useWho() {
52
55
  */
53
56
  export function Who({ open, onOpenChange, narrow }) {
54
57
  const host = useHost();
58
+ const standing = useStanding(useTarget(), open);
55
59
  // The organizations this person acts in; choosing one reloads the page scoped to it.
56
60
  const orgs = host.memberships ?? (host.org ? [host.org] : []);
57
61
  const go = (p) => {
58
62
  onOpenChange(false);
59
63
  host.go(p);
60
64
  };
61
- return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 6, children: [_jsx(Popover.Anchor, { position: "absolute", l: 8, b: 8, width: narrow ? 48 : 256, height: 36, pointerEvents: "none" }), _jsx(Popover.Content, { role: "menu", "aria-label": "Account", items: "stretch", borderWidth: 1, elevation: "$2", px: "$1", py: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person?.email || host.person?.name || '' }), orgs.length ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Organization" }), _jsx(YStack, { role: "radiogroup", "aria-label": "Organizations", gap: "$1", children: orgs.map((o) => (_jsx(MenuRow, { label: o, active: o === host.org, onPress: () => {
65
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 6, children: [_jsx(Popover.Anchor, { position: "absolute", l: 8, b: 8, width: narrow ? 48 : 256, height: 36, pointerEvents: "none" }), _jsx(Popover.Content, { role: "menu", "aria-label": "Account", items: "stretch", borderWidth: 1, elevation: "$2", px: "$1", py: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person?.email || host.person?.name || '' }), _jsx(MenuRule, {}), _jsx(Meter, { read: standing, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), orgs.length ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Organization" }), _jsx(YStack, { role: "radiogroup", "aria-label": "Organizations", gap: "$1", children: orgs.map((o) => (_jsx(MenuRow, { label: o, active: o === host.org, onPress: () => {
62
66
  onOpenChange(false);
63
67
  if (o !== host.org)
64
68
  host.chooseOrg?.(o);
65
- } }, o))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", icon: _jsx(Settings, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(MenuRow, { label: "Usage", icon: _jsx(Gauge, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(MenuRow, { label: "View all plans", icon: _jsx(Sparkles, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(MenuRow, { label: "Get help", icon: _jsx(LifeBuoy, { size: 16 }), onPress: () => {
69
+ } }, o))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", icon: _jsx(Settings, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(MenuRow, { label: "Usage", icon: _jsx(Gauge, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(MenuRow, { label: "Billing", icon: _jsx(CreditCard, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(MenuRow, { label: "View all plans", icon: _jsx(Sparkles, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(MenuRow, { label: "Get help", icon: _jsx(LifeBuoy, { size: 16 }), onPress: () => {
66
70
  onOpenChange(false);
67
71
  window.open(DOCS, '_blank', 'noopener,noreferrer');
68
72
  } }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuRow, { label: "Log out", icon: _jsx(LogOut, { size: 16 }), onPress: () => {
package/lib/forge.js CHANGED
@@ -4,19 +4,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  *
5
5
  * A project is a repository, linked from GitHub or started from a template;
6
6
  * choosing one opens its workspace, where every run works on it. Issues are
7
- * the forge's, filed under a board or a repository, and a repository's GitHub
8
- * issues sync onto it. Choosing an issue opens New with that repository and
9
- * the issue as the ask.
7
+ * the forge's and the task index's, merged (merge.ts): the same issue read
8
+ * from two boards is one row. A repository's GitHub issues sync onto it.
9
+ * Choosing an issue opens New on the issue's own repository — its GitHub
10
+ * address, or the forge repository it is filed on, never a board's key — with
11
+ * the issue as the ask; an issue no project answers to asks which one.
10
12
  */
11
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
12
14
  import { ArrowRight, Check, ChevronDown, CircleCheck, CircleDot, CircleSlash, ExternalLink, FolderGit2, GitBranch, Github, GitPullRequest, LayoutTemplate, RefreshCw, } from '@hanzogui/lucide-icons-2';
13
- import { Button, DropdownMenu, Input } from '@hanzo/ui';
14
- import { useEffect, useState } from 'react';
15
+ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
16
+ import { useEffect, useMemo, useState } from 'react';
15
17
  import { ago } from './ago.js';
16
18
  import { codebases } from './api/codebases.js';
17
19
  import { Automations } from './automations.js';
18
- import { boards, closed, inProject, issues, projectOf } from './api/work.js';
19
- import { boardKey, pinCodebase, readPending, writePending } from './choice.js';
20
+ import { boards, everything, sources } from './api/work.js';
21
+ import { boardKey, pinRepo, readPending, writePending } from './choice.js';
20
22
  import { useKept, useRead } from './data.js';
21
23
  import { useHost, useTarget } from './host.js';
22
24
  import { path } from './route.js';
@@ -25,6 +27,7 @@ import { Sync } from './sync.js';
25
27
  import { connect, connection, grants, syncIssues } from './api/github.js';
26
28
  import { here, useBack } from './back.js';
27
29
  import { Out } from './out.js';
30
+ import { ask, assemble, attach, closes, facets, handle, home, keyOf, matches, pickProjects, place } from './merge.js';
28
31
  function Note({ children }) {
29
32
  return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
30
33
  }
@@ -164,34 +167,107 @@ function GitHubLink({ onSync }) {
164
167
  const KINDS = { all: 'Issues and pull requests', issue: 'Issues', pr: 'Pull requests' };
165
168
  const ORIGINS = { all: 'Every source', github: 'GitHub', hanzo: 'Hanzo' };
166
169
  const ORDERS = { newest: 'Newest', oldest: 'Oldest', updated: 'Recently updated' };
167
- /** Mirrored from GitHub: filed by the forge's `git` source with its page on github.com. */
168
- const fromGitHub = (w) => /^https:\/\/github\.com\//.test(w.url);
170
+ /** Mirrored from GitHub: the task index carried it from GitHub (`extRef` github:…). */
171
+ const fromGitHub = (i) => i.sources.includes('github');
172
+ /** Done and canceled are closed; every other column is open work. */
173
+ const closed = (i) => i.state === 'closed';
174
+ /** An RFC 3339 time as milliseconds, 0 when there is none. */
175
+ const ms = (iso) => Date.parse(iso) || 0;
176
+ /** The repository an issue is for, once the projects have named it, else the forge's name for it, else its board. */
177
+ const projectOf = (i) => i.repo || i.forge || i.board;
178
+ /**
179
+ * Whether `i` is filed under the project `key`: its board, its forge
180
+ * repository, or its repository by address or by name — either case. A board
181
+ * and the repository it is named for are one project, as the forge files them.
182
+ */
183
+ function inProject(i, key) {
184
+ const k = key.toLowerCase();
185
+ return [i.board, i.forge, i.repo, i.repo.split('/')[1] ?? ''].some((v) => v !== '' && v.toLowerCase() === k);
186
+ }
187
+ /** How a person refers to an issue: `owner/name#N` on its own repository, the board's handle otherwise, '' with neither. */
188
+ function ref(i) {
189
+ const tag = handle(i);
190
+ const at = i.repo || i.forge;
191
+ return tag.startsWith('#') && at ? `${at}${tag}` : tag;
192
+ }
169
193
  /** A dropdown that picks one of `options`, its trigger naming the choice. */
170
194
  function Pick({ label, value, options, onPick }) {
171
195
  return (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": label, items: "center", gap: "$1", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: options[value] }), _jsx(ChevronDown, { size: 12 })] }), items: Object.keys(options).map((k) => ({
172
- key: k,
196
+ key: k || '*',
173
197
  label: options[k],
174
198
  icon: k === value ? _jsx(Check, { size: 14 }) : _jsx(YStack, { width: 14 }),
175
199
  onSelect: () => onPick(k),
176
200
  })) }));
177
201
  }
178
- function Mark({ w }) {
179
- if (w.kind === 'pr')
180
- return _jsx(GitPullRequest, { size: 16, color: closed(w) ? '$soft' : '$ink' });
181
- if (w.status === 'canceled')
202
+ /** The words a list carries as a menu: every one of them, or `all`. */
203
+ const menu = (all, values) => Object.fromEntries([['', all], ...values.map((v) => [v, v])]);
204
+ function Mark({ i }) {
205
+ if (i.pull)
206
+ return _jsx(GitPullRequest, { size: 16, color: closed(i) ? '$soft' : '$ink' });
207
+ if (i.status === 'canceled')
182
208
  return _jsx(CircleSlash, { size: 16, color: "$soft" });
183
- if (w.status === 'done')
209
+ if (closed(i))
184
210
  return _jsx(CircleCheck, { size: 16, color: "$soft" });
185
211
  return _jsx(CircleDot, { size: 16, color: "$ink" });
186
212
  }
187
213
  function Tag({ children }) {
188
214
  return (_jsx(XStack, { px: "$2", py: 1, rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: children }) }));
189
215
  }
216
+ const NONE = { hub: [], linked: [], held: [], sites: [], owners: [], next: '', unread: [], missing: [] };
217
+ /**
218
+ * Pressing an issue: New, on the issue's own repository, holding the issue. A
219
+ * GitHub issue runs on GitHub, where its pull request closes it; a forge issue
220
+ * on the forge. A repository not among the projects read so far is looked up by
221
+ * its address; an issue no project answers to asks which one it is for — a
222
+ * board's key or a Linear team's is never taken for a repository.
223
+ */
224
+ function useOpen(list, work) {
225
+ const host = useHost();
226
+ const t = useTarget();
227
+ const [asking, setAsking] = useState(null);
228
+ const [finding, setFinding] = useState('');
229
+ const go = (i, p) => {
230
+ // A project the person picked for an issue that is not its own closes nothing there.
231
+ const own = place(i, [p]) !== null;
232
+ const n = own ? closes(i, p) : 0;
233
+ pinRepo(host.org, p, ask(i, n), n, own ? home(i, p) : undefined);
234
+ setAsking(null);
235
+ host.go('');
236
+ };
237
+ const open = async (i) => {
238
+ const p = place(i, list);
239
+ if (p && (p.forge || p.linked))
240
+ return go(i, p);
241
+ if (i.repo) {
242
+ setFinding(i.key);
243
+ const [owner = '', name = ''] = i.repo.split('/');
244
+ const found = await sources(t, { q: name, owner })
245
+ .then((s) => assemble({ granted: s.hub, linked: s.linked, held: s.held, sites: s.sites, work }).find((x) => x.key === keyOf(owner, name)) ?? null)
246
+ .catch(() => null);
247
+ setFinding('');
248
+ if (found && (found.forge || found.linked))
249
+ return go(i, found);
250
+ }
251
+ setAsking(i);
252
+ };
253
+ const dialog = _jsx(Which, { issue: asking, list: list, onPick: (p) => asking && go(asking, p), onClose: () => setAsking(null) });
254
+ return { open: (i) => void open(i), finding, dialog };
255
+ }
256
+ /** Asks which project an issue is for, from the org's own list: never a free string. */
257
+ function Which({ issue, list, onPick, onClose }) {
258
+ const [q, setQ] = useState('');
259
+ const shown = pickProjects(list.filter((p) => p.forge || p.linked), { q }).slice(0, 50);
260
+ const named = issue?.repo || issue?.hint || '';
261
+ return (_jsx(Dialog, { open: issue !== null, onOpenChange: (o) => (o ? null : onClose()), children: _jsxs(DialogContent, { maxW: 480, gap: "$3", children: [_jsx(DialogTitle, { children: "Which project is this for?" }), _jsx(SizableText, { size: "$2", color: "$soft", children: named
262
+ ? `“${named}” is not one of this organization’s projects. Choose the repository the run works on.`
263
+ : `This issue names no repository${issue?.board ? `: it is on the ${issue.board} board` : ''}. Choose the one the run works on.` }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find a project\u2026", "aria-label": "Find a project" }), _jsx(YStack, { gap: "$1", maxH: 320, overflow: "scroll", children: shown.length === 0 ? (_jsx(Note, { children: list.length ? 'Nothing matches.' : 'No projects are linked yet.' })) : (shown.map((p) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${p.owner}/${p.name}`, onPress: () => onPick(p), px: "$2.5", py: "$2", rounded: "$3", items: "center", justify: "flex-start", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, shrink: 0, children: `${p.owner}/` }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, shrink: 1, children: p.name })] }, p.key)))) }), _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "outline", onPress: onClose, children: "Cancel" }) })] }) }));
264
+ }
190
265
  /**
191
266
  * Every project's issues, read the way GitHub reads them: open or closed, one
192
- * project or all of them, narrowed by words, kind, source and order. Each project
193
- * syncs its own issues from GitHub; choosing an issue starts the next run on its
194
- * codebase with the issue as the ask.
267
+ * project or all of them, narrowed by words, kind, source, label and assignee,
268
+ * and ordered. The same issue on two boards is one row. Each project syncs its
269
+ * own issues from GitHub; choosing an issue starts the next run on the issue's
270
+ * own repository with the issue as the ask.
195
271
  */
196
272
  function Issues() {
197
273
  const host = useHost();
@@ -202,12 +278,19 @@ function Issues() {
202
278
  const [kind, setKind] = useState('all');
203
279
  const [origin, setOrigin] = useState('all');
204
280
  const [order, setOrder] = useState('newest');
281
+ const [label, setLabel] = useState('');
282
+ const [assignee, setAssignee] = useState('');
205
283
  const [q, setQ] = useState('');
206
284
  const [pulled, setPulled] = useState(0);
207
285
  const [busy, setBusy] = useState(false);
208
286
  const [said, setSaid] = useState(null);
209
- const list = useRead(signed ? () => issues(t) : null, [], [t, signed, pulled]);
287
+ const list = useRead(signed ? () => everything(t) : null, [], [t, signed, pulled]);
210
288
  const known = useRead(signed ? () => boards(t) : null, [], [t, signed]);
289
+ // What the org's projects are made from, so an issue is placed on its own repository.
290
+ const made = useRead(signed ? () => sources(t) : null, NONE, [t, signed]);
291
+ const projects = useMemo(() => assemble({ granted: made.value.hub, linked: made.value.linked, held: made.value.held, sites: made.value.sites, work: list.value }), [made.value, list.value]);
292
+ const work = useMemo(() => attach(list.value, projects), [list.value, projects]);
293
+ const { open, finding, dialog } = useOpen(projects, list.value);
211
294
  // Another screen chose the board (the rail's Issues chooses every board) while this one is open.
212
295
  useEffect(() => {
213
296
  const on = (e) => setBoard(e.detail);
@@ -215,37 +298,38 @@ function Issues() {
215
298
  return () => window.removeEventListener('hanzo-board', on);
216
299
  }, [setBoard]);
217
300
  useEffect(() => setSaid(null), [board]);
218
- // The projects: every board the forge names, and every repository an issue is filed under.
219
- const projects = new Map();
301
+ // The projects: every board the forge names, and every repository an issue is filed under that no board is named for.
302
+ const column = new Map();
220
303
  for (const b of known.value)
221
- projects.set(b.key.toLowerCase(), { key: b.key, name: b.name, open: 0 });
222
- for (const w of list.value) {
223
- const key = projectOf(w);
224
- if (!key)
225
- continue;
226
- const p = projects.get(key.toLowerCase()) ?? { key, name: key, open: 0 };
227
- if (!closed(w))
228
- p.open++;
229
- projects.set(key.toLowerCase(), p);
304
+ column.set(b.key.toLowerCase(), { key: b.key, name: b.name, open: 0 });
305
+ for (const i of work) {
306
+ const key = projectOf(i);
307
+ if (key && ![...column.values()].some((p) => inProject(i, p.key)))
308
+ column.set(key.toLowerCase(), { key, name: key, open: 0 });
230
309
  }
231
- const rows = [...projects.values()].sort((a, b) => a.name.localeCompare(b.name));
232
- const current = board ? projects.get(board.toLowerCase()) : undefined;
310
+ for (const p of column.values())
311
+ p.open = work.filter((i) => !closed(i) && inProject(i, p.key)).length;
312
+ const rows = [...column.values()].sort((a, b) => a.name.localeCompare(b.name));
313
+ const current = board ? column.get(board.toLowerCase()) : undefined;
233
314
  const title = current?.name ?? board;
234
- const needle = q.trim().toLowerCase();
235
- const scoped = list.value
236
- .filter((w) => !board || inProject(w, board))
237
- .filter((w) => kind === 'all' || (kind === 'pr' ? w.kind === 'pr' : w.kind !== 'pr'))
238
- .filter((w) => origin === 'all' || (origin === 'github') === fromGitHub(w))
239
- .filter((w) => !needle || [w.title, w.identifier, w.assignee, projectOf(w), ...w.labels].join(' ').toLowerCase().includes(needle));
240
- const opened = scoped.filter((w) => !closed(w));
315
+ const inBoard = work.filter((i) => !board || inProject(i, board));
316
+ const { labels, assignees } = facets(inBoard);
317
+ const scoped = inBoard
318
+ .filter((i) => kind === 'all' || (kind === 'pr') === i.pull)
319
+ .filter((i) => origin === 'all' || (origin === 'github') === fromGitHub(i))
320
+ .filter((i) => !label || i.labels.some((l) => l.toLowerCase() === label.toLowerCase()))
321
+ .filter((i) => !assignee || i.assignees.some((a) => a.toLowerCase() === assignee.toLowerCase()))
322
+ .filter((i) => matches(q, i.title, ref(i), projectOf(i), ...i.assignees, ...i.labels));
323
+ const opened = scoped.filter((i) => !closed(i));
241
324
  const shut = scoped.filter(closed);
242
325
  const shown = (state === 'open' ? opened : shut).slice().sort((a, b) => {
243
326
  if (order === 'updated')
244
- return (b.updated || b.created) - (a.updated || a.created);
327
+ return ms(b.updated || b.created) - ms(a.updated || a.created);
245
328
  if (order === 'oldest')
246
- return a.created - b.created;
247
- return b.created - a.created;
329
+ return ms(a.created) - ms(b.created);
330
+ return ms(b.created) - ms(a.created);
248
331
  });
332
+ const narrowed = q.trim() !== '' || kind !== 'all' || origin !== 'all' || label !== '' || assignee !== '';
249
333
  const pull = async () => {
250
334
  setBusy(true);
251
335
  setSaid(null);
@@ -274,37 +358,21 @@ function Issues() {
274
358
  setSaid({ text: e.message });
275
359
  }
276
360
  };
277
- const open = (w) => {
278
- const name = w.repo || w.project;
279
- if (name) {
280
- pinCodebase(host.org, {
281
- org: host.org ?? '',
282
- name,
283
- description: '',
284
- branch: 'main',
285
- public: false,
286
- clone: '',
287
- updated: '',
288
- branches: ['main'],
289
- }, `${w.identifier ? `${w.identifier} ` : ''}${w.title}`.trim());
290
- }
291
- host.go('');
292
- };
293
361
  const chip = (key, name, count) => {
294
362
  const on = key.toLowerCase() === board.toLowerCase();
295
363
  return (_jsxs(XStack, { render: "button", "aria-label": key ? `Show ${name}` : 'Show every project', "aria-pressed": on, onPress: () => setBoard(key), items: "center", gap: "$2", px: "$2.5", py: "$1.5", rounded: "$3", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, shrink: 0, children: [_jsx(SizableText, { flex: 1, size: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, style: { textAlign: 'left' }, children: name }), count ? (_jsx(SizableText, { size: "$1", color: "$soft", children: count })) : null] }, key || '*'));
296
364
  };
297
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 240, children: [_jsx(SizableText, { render: "h1", size: "$6", fontWeight: "500", color: "$ink", children: board ? `Issues · ${title}` : 'Issues' }), _jsx(SizableText, { size: "$2", color: "$soft", children: board ? `Work on ${title}. Choosing an issue starts the next run on that codebase.` : 'Work across every project. Choosing an issue starts the next run on that codebase.' })] }), _jsxs(Button, { size: "sm", variant: "outline", disabled: !signed || busy, onPress: () => void pull(), children: [_jsx(Github, { size: 14 }), busy ? 'Syncing…' : board ? `Sync ${title} from GitHub` : 'Sync all from GitHub'] })] }), said ? (_jsxs(XStack, { items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(Note, { children: said.text }), said.connect ? (_jsxs(Button, { size: "sm", variant: "primary", onPress: () => void link(), children: [_jsx(Github, { size: 14 }), "Connect GitHub"] })) : null] })) : null, _jsxs(XStack, { gap: "$5", items: "flex-start", "$max-md": { flexDirection: 'column', gap: '$3' }, children: [_jsxs(YStack, { render: "nav", "aria-label": "Projects", width: 220, shrink: 0, gap: "$0.5", "$max-md": { width: '100%', flexDirection: 'row', overflow: 'scroll', gap: '$1' }, children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2.5", pb: "$1", "$max-md": { display: 'none' }, children: "Projects" }), chip('', 'All projects', list.value.filter((w) => !closed(w)).length), rows.map((p) => chip(p.key, p.name, p.open))] }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$3", width: "100%", children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search issues", "aria-label": "Search issues", disabled: !signed }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", bg: "$panel", flexWrap: "wrap", children: [_jsxs(XStack, { gap: "$3", items: "center", flex: 1, minW: 0, children: [_jsxs(XStack, { render: "button", "aria-label": "Open issues", "aria-pressed": state === 'open', items: "center", gap: "$1.5", onPress: () => setState('open'), children: [_jsx(CircleDot, { size: 14, color: state === 'open' ? '$ink' : '$soft' }), _jsx(SizableText, { size: "$2", color: state === 'open' ? '$ink' : '$soft', fontWeight: state === 'open' ? '500' : '400', children: `${opened.length} Open` })] }), _jsxs(XStack, { render: "button", "aria-label": "Closed issues", "aria-pressed": state === 'closed', items: "center", gap: "$1.5", onPress: () => setState('closed'), children: [_jsx(Check, { size: 14, color: state === 'closed' ? '$ink' : '$soft' }), _jsx(SizableText, { size: "$2", color: state === 'closed' ? '$ink' : '$soft', fontWeight: state === 'closed' ? '500' : '400', children: `${shut.length} Closed` })] })] }), _jsxs(XStack, { gap: "$1", items: "center", flexWrap: "wrap", shrink: 1, minW: 0, children: [_jsx(Pick, { label: "Kind", value: kind, options: KINDS, onPick: setKind }), _jsx(Pick, { label: "Source", value: origin, options: ORIGINS, onPick: setOrigin }), _jsx(Pick, { label: "Sort", value: order, options: ORDERS, onPick: setOrder })] })] }), shown.length === 0 ? (_jsxs(YStack, { px: "$3", py: "$6", gap: "$3", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Note, { children: state === 'open'
298
- ? needle || kind !== 'all' || origin !== 'all'
299
- ? 'No open issues match.'
300
- : 'Nothing open.'
301
- : 'Nothing closed.' }), list.value.length === 0 ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", justify: "center", children: [_jsxs(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => void pull(), children: [_jsx(Github, { size: 14 }), "Sync from GitHub"] }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "Start a new run" })] })) : null] })) : (shown.map((w) => (_jsxs(XStack, { items: "center", gap: "$2", pr: "$3", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { render: "button", "aria-label": `Build ${w.identifier || w.title}`, onPress: () => open(w), flex: 1, minW: 0, items: "flex-start", gap: "$3", pl: "$3", py: "$2.5", focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(YStack, { pt: 2, children: _jsx(Mark, { w: w }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", items: "flex-start", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", maxW: "100%", children: [_jsx(SizableText, { size: "$3", color: "$ink", fontWeight: "500", numberOfLines: 1, shrink: 1, children: w.title }), STATUS[w.status] && !closed(w) && w.status !== 'todo' ? _jsx(Tag, { children: STATUS[w.status] }) : null, !STATUS[w.status] ? _jsx(Tag, { children: w.status }) : null, w.priority !== 'none' && w.priority ? _jsx(Tag, { children: w.priority }) : null, w.labels.slice(0, 4).map((l) => (_jsx(Tag, { children: l }, l)))] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [
302
- w.identifier || w.kind,
303
- !board && projectOf(w) ? projectOf(w) : '',
304
- w.created ? `opened ${ago(new Date(w.created).toISOString())}` : '',
305
- w.assignee ? `assigned to ${w.assignee}` : '',
306
- fromGitHub(w) ? 'GitHub' : '',
307
- ]
308
- .filter(Boolean)
309
- .join(' · ') })] })] }), w.url ? (_jsx(Out, { href: w.url, label: `Open ${w.identifier || w.title} on ${fromGitHub(w) ? 'GitHub' : 'the forge'}`, children: _jsx(ExternalLink, { size: 14, opacity: 0.6 }) })) : null] }, w.id))))] }))] })] })] }) }));
365
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 240, children: [_jsx(SizableText, { render: "h1", size: "$6", fontWeight: "500", color: "$ink", children: board ? `Issues · ${title}` : 'Issues' }), _jsx(SizableText, { size: "$2", color: "$soft", children: board ? `Work on ${title}. Choosing an issue starts the next run on its own repository.` : 'Work across every project. Choosing an issue starts the next run on its own repository.' })] }), _jsxs(Button, { size: "sm", variant: "outline", disabled: !signed || busy, onPress: () => void pull(), children: [_jsx(Github, { size: 14 }), busy ? 'Syncing…' : board ? `Sync ${title} from GitHub` : 'Sync all from GitHub'] })] }), said ? (_jsxs(XStack, { items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(Note, { children: said.text }), said.connect ? (_jsxs(Button, { size: "sm", variant: "primary", onPress: () => void link(), children: [_jsx(Github, { size: 14 }), "Connect GitHub"] })) : null] })) : null, _jsxs(XStack, { gap: "$5", items: "flex-start", "$max-md": { flexDirection: 'column', gap: '$3' }, children: [_jsxs(YStack, { render: "nav", "aria-label": "Projects", width: 220, shrink: 0, gap: "$0.5", "$max-md": { width: '100%', flexDirection: 'row', overflow: 'scroll', gap: '$1' }, children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2.5", pb: "$1", "$max-md": { display: 'none' }, children: "Projects" }), chip('', 'All projects', work.filter((i) => !closed(i)).length), rows.map((p) => chip(p.key, p.name, p.open))] }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$3", width: "100%", children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search issues", "aria-label": "Search issues", disabled: !signed }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", bg: "$panel", flexWrap: "wrap", children: [_jsxs(XStack, { gap: "$3", items: "center", flex: 1, minW: 0, children: [_jsxs(XStack, { render: "button", "aria-label": "Open issues", "aria-pressed": state === 'open', items: "center", gap: "$1.5", onPress: () => setState('open'), children: [_jsx(CircleDot, { size: 14, color: state === 'open' ? '$ink' : '$soft' }), _jsx(SizableText, { size: "$2", color: state === 'open' ? '$ink' : '$soft', fontWeight: state === 'open' ? '500' : '400', children: `${opened.length} Open` })] }), _jsxs(XStack, { render: "button", "aria-label": "Closed issues", "aria-pressed": state === 'closed', items: "center", gap: "$1.5", onPress: () => setState('closed'), children: [_jsx(Check, { size: 14, color: state === 'closed' ? '$ink' : '$soft' }), _jsx(SizableText, { size: "$2", color: state === 'closed' ? '$ink' : '$soft', fontWeight: state === 'closed' ? '500' : '400', children: `${shut.length} Closed` })] })] }), _jsxs(XStack, { gap: "$1", items: "center", flexWrap: "wrap", shrink: 1, minW: 0, children: [_jsx(Pick, { label: "Label", value: label, options: menu('Any label', labels), onPick: setLabel }), _jsx(Pick, { label: "Assignee", value: assignee, options: menu('Anyone', assignees), onPick: setAssignee }), _jsx(Pick, { label: "Kind", value: kind, options: KINDS, onPick: setKind }), _jsx(Pick, { label: "Source", value: origin, options: ORIGINS, onPick: setOrigin }), _jsx(Pick, { label: "Sort", value: order, options: ORDERS, onPick: setOrder })] })] }), shown.length === 0 ? (_jsxs(YStack, { px: "$3", py: "$6", gap: "$3", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Note, { children: state === 'open' ? (narrowed ? 'No open issues match.' : 'Nothing open.') : 'Nothing closed.' }), list.value.length === 0 ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", justify: "center", children: [_jsxs(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => void pull(), children: [_jsx(Github, { size: 14 }), "Sync from GitHub"] }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "Start a new run" })] })) : null] })) : (shown.map((i) => {
366
+ const named = ref(i);
367
+ return (_jsxs(XStack, { items: "center", gap: "$2", pr: "$3", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { render: "button", "aria-label": `Build ${named || i.title}`, onPress: () => open(i), disabled: finding === i.key && i.key !== '', flex: 1, minW: 0, items: "flex-start", gap: "$3", pl: "$3", py: "$2.5", focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(YStack, { pt: 2, children: _jsx(Mark, { i: i }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", items: "flex-start", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", maxW: "100%", children: [_jsx(SizableText, { size: "$3", color: "$ink", fontWeight: "500", numberOfLines: 1, shrink: 1, children: i.title }), STATUS[i.status] && !closed(i) && i.status !== 'todo' ? _jsx(Tag, { children: STATUS[i.status] }) : null, !STATUS[i.status] ? _jsx(Tag, { children: i.status }) : null, i.priority !== 'none' && i.priority ? _jsx(Tag, { children: i.priority }) : null, i.labels.slice(0, 4).map((l) => (_jsx(Tag, { children: l }, l)))] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [
368
+ named || (i.pull ? 'pr' : i.kind),
369
+ !board && projectOf(i) && !named.startsWith(projectOf(i)) ? projectOf(i) : '',
370
+ i.created ? `opened ${ago(i.created)}` : '',
371
+ i.assignees.length ? `assigned to ${i.assignees.join(', ')}` : '',
372
+ fromGitHub(i) ? 'GitHub' : '',
373
+ finding === i.key && i.key !== '' ? 'opening…' : '',
374
+ ]
375
+ .filter(Boolean)
376
+ .join(' · ') })] })] }), i.url ? (_jsx(Out, { href: i.url, label: `Open ${named || i.title} on ${fromGitHub(i) ? 'GitHub' : 'the forge'}`, children: _jsx(ExternalLink, { size: 14, opacity: 0.6 }) })) : null] }, i.key || `${i.board}:${i.title}`));
377
+ }))] }))] })] })] }), dialog] }));
310
378
  }
package/lib/index.d.ts CHANGED
@@ -9,14 +9,40 @@
9
9
  * and at the foot the Slack card and the account menu (`Slack`, `Who`), under
10
10
  * `HostProvider`. `Grip` sizes that rail: the edge the builder sizes its own
11
11
  * chat with.
12
+ *
13
+ * The plan, once for every surface that shows one: `label` names it (`Max` with
14
+ * `20x` beside it), `useStanding` reads where an account stands, `Meter` is the
15
+ * block an account menu leads with, `Plan` the Usage view and `Credits` the
16
+ * Billing one. Each reads as a `Target`, so a host without a builder mounts it.
17
+ * The pure half — naming, shares, the banner's words — is also `@hanzo/build/plan`,
18
+ * which loads in Node without the components.
12
19
  */
13
- export { Builder, nav } from './builder.tsx';
20
+ export { Builder } from './builder.tsx';
21
+ export { nav, NEW, type Group } from './nav.tsx';
14
22
  export { DevSection, DOTS, useSessions } from './section.tsx';
15
23
  export { HostProvider } from './host.tsx';
16
24
  export { Slack, useWho, Who } from './foot.tsx';
17
25
  export { Grip, type GripProps } from './grip.tsx';
18
26
  export { administers } from './claims.ts';
27
+ export { Meter, Title } from './meter.tsx';
28
+ export { Plan } from './settings/plan.tsx';
29
+ export { Credits, SEPARATE } from './settings/credits.tsx';
30
+ export { useStanding, rows, left, type Standing, type Row } from './standing.ts';
31
+ export { kind, label, said, share, spent, ways, when, type Label, type Share, type State } from './plan.ts';
32
+ export type { Limits } from './api/limits.ts';
33
+ export type { Target } from './api/call.ts';
19
34
  export type { Host, Person } from './host.tsx';
20
35
  export type { Session, Status } from './api/sessions.ts';
21
36
  export type { Read } from './data.ts';
22
- export { path, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
37
+ export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
38
+ /**
39
+ * What Chat draws the same way (@hanzo/rooms imports these): the model and
40
+ * effort (`useMind`, one choice kept for both, changed by `Tune`), the composer
41
+ * (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
42
+ * over a `Deck` of tabs).
43
+ */
44
+ export { catalog, EFFORTS, FIRST, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
45
+ export { cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, Tune, useEar, type PromptOptions } from './prompt.tsx';
46
+ export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker, type Artifact, type Listen } from './reply.tsx';
47
+ export { Page, Panel, PanelToggle, type Kind, type PanelProps } from './panel.tsx';
48
+ export { address, name, PAGE, useDeck, useKey, usePanel, useShown, type Deck, type Tab, type Tabs } from './tabs.ts';
package/lib/index.js CHANGED
@@ -9,11 +9,35 @@
9
9
  * and at the foot the Slack card and the account menu (`Slack`, `Who`), under
10
10
  * `HostProvider`. `Grip` sizes that rail: the edge the builder sizes its own
11
11
  * chat with.
12
+ *
13
+ * The plan, once for every surface that shows one: `label` names it (`Max` with
14
+ * `20x` beside it), `useStanding` reads where an account stands, `Meter` is the
15
+ * block an account menu leads with, `Plan` the Usage view and `Credits` the
16
+ * Billing one. Each reads as a `Target`, so a host without a builder mounts it.
17
+ * The pure half — naming, shares, the banner's words — is also `@hanzo/build/plan`,
18
+ * which loads in Node without the components.
12
19
  */
13
- export { Builder, nav } from './builder.js';
20
+ export { Builder } from './builder.js';
21
+ export { nav, NEW } from './nav.js';
14
22
  export { DevSection, DOTS, useSessions } from './section.js';
15
23
  export { HostProvider } from './host.js';
16
24
  export { Slack, useWho, Who } from './foot.js';
17
25
  export { Grip } from './grip.js';
18
26
  export { administers } from './claims.js';
19
- export { path, route, SESSION, SLUG } from './route.js';
27
+ export { Meter, Title } from './meter.js';
28
+ export { Plan } from './settings/plan.js';
29
+ export { Credits, SEPARATE } from './settings/credits.js';
30
+ export { useStanding, rows, left } from './standing.js';
31
+ export { kind, label, said, share, spent, ways, when } from './plan.js';
32
+ export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
33
+ /**
34
+ * What Chat draws the same way (@hanzo/rooms imports these): the model and
35
+ * effort (`useMind`, one choice kept for both, changed by `Tune`), the composer
36
+ * (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
37
+ * over a `Deck` of tabs).
38
+ */
39
+ export { catalog, EFFORTS, FIRST, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
40
+ export { cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, Tune, useEar } from './prompt.js';
41
+ export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker } from './reply.js';
42
+ export { Page, Panel, PanelToggle } from './panel.js';
43
+ export { address, name, PAGE, useDeck, useKey, usePanel, useShown } from './tabs.js';
package/lib/landing.d.ts CHANGED
@@ -9,16 +9,6 @@ export declare const MODES: readonly [{
9
9
  readonly label: 'Plan';
10
10
  readonly hint: 'Plans the change and writes nothing';
11
11
  }];
12
- export declare const EFFORTS: readonly [{
13
- readonly id: 'low';
14
- readonly label: 'Low';
15
- }, {
16
- readonly id: 'medium';
17
- readonly label: 'Medium';
18
- }, {
19
- readonly id: 'high';
20
- readonly label: 'High';
21
- }];
22
12
  /**
23
13
  * What was typed here and not sent, kept in this tab.
24
14
  *