@hanzo/build 0.2.51 → 0.2.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +109 -55
  2. package/lib/api/billing.d.ts +39 -0
  3. package/lib/api/billing.js +41 -0
  4. package/lib/api/coding.d.ts +4 -1
  5. package/lib/api/coding.js +2 -0
  6. package/lib/api/environment.d.ts +12 -0
  7. package/lib/api/environment.js +39 -1
  8. package/lib/api/github.d.ts +42 -4
  9. package/lib/api/github.js +79 -16
  10. package/lib/api/limits.d.ts +69 -0
  11. package/lib/api/limits.js +86 -0
  12. package/lib/api/links.d.ts +42 -0
  13. package/lib/api/links.js +65 -0
  14. package/lib/api/pref.d.ts +5 -3
  15. package/lib/api/pref.js +0 -3
  16. package/lib/api/turn.d.ts +10 -1
  17. package/lib/api/turn.js +16 -4
  18. package/lib/api/work.d.ts +75 -24
  19. package/lib/api/work.js +132 -27
  20. package/lib/builder.d.ts +0 -10
  21. package/lib/builder.js +21 -54
  22. package/lib/choice.d.ts +21 -0
  23. package/lib/choice.js +40 -1
  24. package/lib/customize/agents.d.ts +1 -1
  25. package/lib/customize/agents.js +21 -19
  26. package/lib/customize/connectors.d.ts +1 -1
  27. package/lib/customize/connectors.js +80 -31
  28. package/lib/customize/index.d.ts +2 -0
  29. package/lib/customize/index.js +23 -19
  30. package/lib/customize/load.d.ts +9 -0
  31. package/lib/customize/load.js +22 -0
  32. package/lib/customize/plugins.d.ts +1 -1
  33. package/lib/customize/plugins.js +21 -19
  34. package/lib/customize/say.d.ts +1 -0
  35. package/lib/customize/say.js +38 -0
  36. package/lib/customize/skills.d.ts +1 -1
  37. package/lib/customize/skills.js +23 -17
  38. package/lib/customize/ui.d.ts +28 -11
  39. package/lib/customize/ui.js +29 -21
  40. package/lib/data.d.ts +13 -0
  41. package/lib/data.js +70 -0
  42. package/lib/desk.d.ts +6 -1
  43. package/lib/desk.js +43 -19
  44. package/lib/environment.d.ts +11 -3
  45. package/lib/environment.js +53 -13
  46. package/lib/foot.js +10 -6
  47. package/lib/forge.js +255 -92
  48. package/lib/host.d.ts +1 -1
  49. package/lib/index.d.ts +28 -2
  50. package/lib/index.js +26 -2
  51. package/lib/landing.d.ts +0 -10
  52. package/lib/landing.js +79 -47
  53. package/lib/merge.d.ts +223 -0
  54. package/lib/merge.js +380 -0
  55. package/lib/meter.d.ts +31 -0
  56. package/lib/meter.js +45 -0
  57. package/lib/mind.d.ts +60 -0
  58. package/lib/mind.js +187 -0
  59. package/lib/nav.d.ts +11 -0
  60. package/lib/nav.js +75 -0
  61. package/lib/panel.d.ts +45 -0
  62. package/lib/panel.js +115 -0
  63. package/lib/pick.d.ts +8 -0
  64. package/lib/pick.js +18 -1
  65. package/lib/plan.d.ts +78 -0
  66. package/lib/plan.js +133 -0
  67. package/lib/project.d.ts +16 -1
  68. package/lib/project.js +100 -32
  69. package/lib/prompt.d.ts +75 -0
  70. package/lib/prompt.js +125 -0
  71. package/lib/prose.js +14 -10
  72. package/lib/reply.d.ts +36 -0
  73. package/lib/reply.js +124 -0
  74. package/lib/route.d.ts +28 -4
  75. package/lib/route.js +48 -1
  76. package/lib/run.js +87 -52
  77. package/lib/section.d.ts +1 -1
  78. package/lib/section.js +7 -11
  79. package/lib/settings/billing.js +16 -9
  80. package/lib/settings/code.js +15 -12
  81. package/lib/settings/credits.d.ts +9 -0
  82. package/lib/settings/credits.js +110 -0
  83. package/lib/settings/plan.d.ts +17 -0
  84. package/lib/settings/plan.js +108 -0
  85. package/lib/settings/usage.js +11 -119
  86. package/lib/shelf.js +3 -3
  87. package/lib/standing.d.ts +44 -0
  88. package/lib/standing.js +64 -0
  89. package/lib/sync.js +23 -9
  90. package/lib/tabs.d.ts +53 -0
  91. package/lib/tabs.js +197 -0
  92. package/lib/tools.d.ts +2 -9
  93. package/lib/tools.js +20 -25
  94. package/lib/transcript.d.ts +10 -1
  95. package/lib/transcript.js +25 -10
  96. package/lib/voice.d.ts +2 -13
  97. package/lib/voice.js +7 -73
  98. package/package.json +20 -3
package/lib/forge.js CHANGED
@@ -1,33 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Codebases, projects and issues, read from the forge and opened in this window.
3
+ * Projects and issues, read from the forge and opened in this window.
4
4
  *
5
- * A codebase is a repository. Choosing one is choosing what the next run works
6
- * on. A project is a board of issues on one of those repositories. Choosing an
7
- * issue opens New with that codebase and the issue as the ask.
5
+ * A project is a repository, linked from GitHub or started from a template;
6
+ * choosing one opens its workspace, where every run works on it. Issues are
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.
8
12
  */
9
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
10
- import { ChevronDown, GitBranch, Plus, Settings } from '@hanzogui/lucide-icons-2';
11
- import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
12
- import { useEffect, useState } from 'react';
14
+ import { ArrowRight, Check, ChevronDown, CircleCheck, CircleDot, CircleSlash, ExternalLink, FolderGit2, GitBranch, Github, GitPullRequest, LayoutTemplate, RefreshCw, } from '@hanzogui/lucide-icons-2';
15
+ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
16
+ import { useEffect, useMemo, useState } from 'react';
13
17
  import { ago } from './ago.js';
14
- import { codebases, create } from './api/codebases.js';
18
+ import { codebases } from './api/codebases.js';
15
19
  import { Automations } from './automations.js';
16
- import { environments } from './api/environment.js';
17
- import { boards, issues } from './api/work.js';
18
- import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './choice.js';
20
+ import { boards, everything, sources } from './api/work.js';
21
+ import { boardKey, pinRepo, readPending, writePending } from './choice.js';
19
22
  import { useKept, useRead } from './data.js';
20
23
  import { useHost, useTarget } from './host.js';
21
24
  import { path } from './route.js';
22
25
  import { Customize } from './customize/index.js';
23
26
  import { Sync } from './sync.js';
24
- import { grants } from './api/github.js';
25
- function Head({ title, says }) {
26
- return (_jsxs(YStack, { gap: "$1", pb: "$4", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }));
27
- }
28
- function Row({ label, onPress, children }) {
29
- return (_jsx(XStack, { render: "button", onPress: onPress, "aria-label": label, items: "center", gap: "$3", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: children }));
30
- }
27
+ import { connect, connection, grants, syncIssues } from './api/github.js';
28
+ import { here, useBack } from './back.js';
29
+ import { Out } from './out.js';
30
+ import { ask, assemble, attach, closes, facets, handle, home, keyOf, matches, pickProjects, place } from './merge.js';
31
31
  function Note({ children }) {
32
32
  return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
33
33
  }
@@ -46,34 +46,34 @@ export function Forge({ screen }) {
46
46
  // The fleet's native servers are Connectors → Discover now; the old address lands there.
47
47
  if (screen === 'mcp')
48
48
  return _jsx(Customize, { tab: "connectors", view: "discover" });
49
- if (screen === 'codebases')
50
- return _jsx(Codebases, {});
51
49
  if (screen === 'projects')
52
50
  return _jsx(Projects, {});
53
51
  return _jsx(Issues, {});
54
52
  }
55
53
  const PAGE = 25;
56
- function Codebases() {
54
+ /**
55
+ * The organization's projects: its repositories on the forge, which arrive one
56
+ * of two ways — linked from GitHub, or started from a template. A project is a
57
+ * repository; opening one opens its workspace at `<org>/<repo>`, and every run
58
+ * there works on that repository. There is no project without one.
59
+ */
60
+ function Projects() {
57
61
  const host = useHost();
58
62
  const t = useTarget();
59
63
  const signed = Boolean(host.person);
60
64
  const [q, setQ] = useState('');
61
- const [making, setMaking] = useState(false);
62
- const [sort, setSort] = useState('name');
63
- const [filter, setFilter] = useState('all');
65
+ const [sort, setSort] = useState('updated');
64
66
  const [page, setPage] = useState(0);
65
67
  const [landed, setLanded] = useState(0);
66
68
  useEffect(() => {
67
69
  setPage(0);
68
- }, [q, sort, filter]);
69
- const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, making, landed]);
70
- // Which codebases have an environment. A codebase absent here has none.
71
- const envs = useRead(signed ? () => environments(t) : null, [], [t, signed]);
72
- const envOf = new Map(envs.value.map((e) => [e.repo, e.state]));
70
+ }, [q, sort]);
71
+ const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, landed]);
73
72
  const needle = q.trim().toLowerCase();
74
73
  const pending = readPending(host.org);
75
74
  const pendingKey = pending.map((p) => p.fullName).join('\n');
76
75
  const syncing = new Set(pending.map((p) => p.name));
76
+ // A repository asked for from GitHub is listed as syncing until the forge holds it.
77
77
  useEffect(() => {
78
78
  const waiting = readPending(host.org);
79
79
  if (!signed || waiting.length === 0)
@@ -110,19 +110,12 @@ function Codebases() {
110
110
  branches: ['main'],
111
111
  })),
112
112
  ]
113
- .filter((c) => {
114
- if (filter === 'ready')
115
- return envOf.get(c.name) === 'ready';
116
- if (filter === 'syncing')
117
- return syncing.has(c.name);
118
- return true;
119
- })
120
113
  .filter((c) => !needle || `${c.org}/${c.name} ${c.description}`.toLowerCase().includes(needle))
121
114
  .slice()
122
115
  .sort((a, b) => {
123
116
  if (sort === 'updated') {
124
- const d = Date.parse(b.updated) - Date.parse(a.updated);
125
- if (Number.isFinite(d) && d !== 0)
117
+ const d = (Date.parse(b.updated) || 0) - (Date.parse(a.updated) || 0);
118
+ if (d !== 0)
126
119
  return d;
127
120
  }
128
121
  return a.name.localeCompare(b.name);
@@ -132,84 +125,254 @@ function Codebases() {
132
125
  const slice = shown.slice(safePage * PAGE, safePage * PAGE + PAGE);
133
126
  const from = shown.length === 0 ? 0 : safePage * PAGE + 1;
134
127
  const to = safePage * PAGE + slice.length;
135
- const open = (c) => {
136
- pinCodebase(host.org, c);
137
- host.go('');
138
- };
139
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Filter repositories", onPress: () => setFilter((f) => (f === 'all' ? 'ready' : f === 'ready' ? 'syncing' : 'all')), px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, hoverStyle: { bg: '$hover' }, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: filter === 'ready' ? 'Ready env' : filter === 'syncing' ? 'Syncing' : 'All repos' }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' }), list.value.length === 0 ? (_jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "Create repository"] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync from GitHub" }), _jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go('-/templates'), children: "Start from template" })] })) : null] })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : ago(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
140
- const row = await create(t, name, description);
141
- pinCodebase(host.org, row);
142
- setMaking(false);
143
- host.go('');
144
- } })] }));
128
+ const none = !list.loading && !list.error && list.value.length === 0 && pending.length === 0;
129
+ const open = (c) => host.go(path({ kind: 'repo', org: c.org || host.org || '', name: c.name }));
130
+ const link = () => host.go(path({ kind: 'screen', screen: 'sync' }));
131
+ const template = () => host.go(path({ kind: 'screen', screen: 'templates' }));
132
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1040, 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: "Projects" }), _jsx(SizableText, { size: "$2", color: "$soft", children: `${host.org ? `${host.org}’s` : 'This organization’s'} repositories on the forge. Open one to work on it.` })] }), _jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsxs(Button, { size: "sm", variant: "outline", onPress: template, children: [_jsx(LayoutTemplate, { size: 14 }), "Create from template"] }), _jsxs(Button, { size: "sm", variant: "primary", onPress: link, children: [_jsx(Github, { size: 14 }), "Link a GitHub repo"] })] })] }), signed ? _jsx(GitHubLink, { onSync: link }) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's projects." })) : list.loading && list.value.length === 0 && pending.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : none ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "No projects yet. A project is a repository: link one from GitHub, or start one from a template." }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsx(Choice, { label: "Link a GitHub repo", icon: _jsx(Github, { size: 20 }), says: "Mirror repositories you already have. They stay in step with GitHub, and their issues can come too.", onPress: link }), _jsx(Choice, { label: "Create from template", icon: _jsx(LayoutTemplate, { size: 20 }), says: "Start from a finished product \u2014 a site, an app, a store \u2014 and make it yours.", onPress: template })] })] })) : (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 320, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find a project\u2026", "aria-label": "Find a project" }) })] }), shown.length === 0 ? (_jsx(Note, { children: "Nothing matches." })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", bg: "$panel", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, "$max-md": { display: 'none' }, children: "Branch" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 120, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Open ${c.org ? `${c.org}/` : ''}${c.name}`, onPress: () => open(c), style: { textAlign: 'left' }, items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(FolderGit2, { size: 15, color: "$soft" }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsxs(XStack, { minW: 0, items: "baseline", children: [c.org ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, shrink: 0, children: `${c.org}/` })) : null, _jsx(SizableText, { size: "$2", color: "$ink", fontWeight: "500", numberOfLines: 1, shrink: 1, children: c.name })] }), c.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: c.description })) : null] }), _jsxs(XStack, { width: 120, items: "center", gap: "$1.5", minW: 0, "$max-md": { display: 'none' }, children: [_jsx(GitBranch, { size: 12, color: "$soft" }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: c.branch })] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : ago(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }))] }) }));
145
133
  }
146
- function NewRepo({ open, onOpenChange, onCreate, }) {
147
- const [name, setName] = useState('');
148
- const [description, setDescription] = useState('');
134
+ /** One of the two ways in, as a card you press. */
135
+ function Choice({ label, icon, says, onPress }) {
136
+ return (_jsxs(YStack, { render: "button", "aria-label": label, onPress: onPress, flex: 1, minW: 240, gap: "$2", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", items: "flex-start", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [icon, _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(SizableText, { size: "$4", color: "$ink", fontWeight: "500", children: label }), _jsx(ArrowRight, { size: 14 })] }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'left' }, children: says })] }));
137
+ }
138
+ /**
139
+ * The person's GitHub connection, in one line: who it is connected as and the
140
+ * way to sync, or the one press that connects it. GitHub returns here, and this
141
+ * page finishes the connect (../back.ts).
142
+ */
143
+ function GitHubLink({ onSync }) {
144
+ const host = useHost();
145
+ const t = useTarget();
146
+ const me = useRead(() => connection(t), null, [t]);
147
+ const [note, setNote] = useState('');
149
148
  const [busy, setBusy] = useState(false);
150
- const [error, setError] = useState('');
151
- useEffect(() => {
152
- if (!open) {
153
- setName('');
154
- setDescription('');
155
- setError('');
156
- }
157
- }, [open]);
158
- const submit = async () => {
149
+ useBack(t, true, setNote, me.reload);
150
+ const c = me.value;
151
+ // A connection that cannot be read says nothing here: Sync from GitHub above still works.
152
+ if (me.error || !c || !c.configured)
153
+ return note ? _jsx(Note, { children: note }) : null;
154
+ const link = async () => {
159
155
  setBusy(true);
160
- setError('');
156
+ setNote('');
161
157
  try {
162
- await onCreate(name, description);
163
- setName('');
164
- setDescription('');
158
+ host.open(await connect(t, here()));
165
159
  }
166
160
  catch (e) {
167
- setError(e.message);
168
- }
169
- finally {
161
+ setNote(e.message);
170
162
  setBusy(false);
171
163
  }
172
164
  };
173
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogTitle, { children: "New repository" }), _jsxs(YStack, { gap: "$3", pt: "$3", children: [_jsx(Input, { value: name, onChangeText: setName, placeholder: "Name", "aria-label": "Repository name" }), _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "Description", "aria-label": "Description" }), error ? _jsx(Note, { children: error }) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] })] }) }));
165
+ return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", children: [_jsx(Github, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 200, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: c.connected ? `GitHub connected as @${c.login}` : 'Connect GitHub to bring your repositories here' }), _jsx(SizableText, { size: "$1", color: "$soft", children: note || (c.connected ? 'Choose repositories to mirror; each stays in step with GitHub.' : 'One step on GitHub, then pick the repositories to sync.') })] }), c.connected ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: onSync, children: [_jsx(RefreshCw, { size: 14 }), "Sync repositories"] })) : (_jsxs(Button, { size: "sm", variant: "primary", disabled: busy, onPress: () => void link(), children: [_jsx(Github, { size: 14 }), busy ? 'Opening GitHub…' : 'Connect GitHub'] }))] }));
174
166
  }
175
- function Projects() {
167
+ const KINDS = { all: 'Issues and pull requests', issue: 'Issues', pr: 'Pull requests' };
168
+ const ORIGINS = { all: 'Every source', github: 'GitHub', hanzo: 'Hanzo' };
169
+ const ORDERS = { newest: 'Newest', oldest: 'Oldest', updated: 'Recently updated' };
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
+ }
193
+ /** A dropdown that picks one of `options`, its trigger naming the choice. */
194
+ function Pick({ label, value, options, onPick }) {
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) => ({
196
+ key: k || '*',
197
+ label: options[k],
198
+ icon: k === value ? _jsx(Check, { size: 14 }) : _jsx(YStack, { width: 14 }),
199
+ onSelect: () => onPick(k),
200
+ })) }));
201
+ }
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')
208
+ return _jsx(CircleSlash, { size: 16, color: "$soft" });
209
+ if (closed(i))
210
+ return _jsx(CircleCheck, { size: 16, color: "$soft" });
211
+ return _jsx(CircleDot, { size: 16, color: "$ink" });
212
+ }
213
+ function Tag({ children }) {
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 }) }));
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) {
176
225
  const host = useHost();
177
226
  const t = useTarget();
178
- const signed = Boolean(host.person);
179
- const list = useRead(signed ? () => boards(t) : null, [], [t, signed]);
180
- const open = (b) => {
181
- pinBoard(host.org, b.key);
182
- host.go(path({ kind: 'screen', screen: 'issues' }));
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);
183
252
  };
184
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Projects", says: "Boards on the forge. A board is a repository that has work on it." }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's boards." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "No boards yet. A repository shows up here once it has an issue." }), _jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })), children: "View repositories" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync from GitHub" })] })] })) : (_jsx(YStack, { gap: "$2", children: list.value.map((b) => (_jsx(Row, { label: `Open ${b.name}`, onPress: () => open(b), children: _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: b.name }), b.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: b.description })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: b.key }))] }) }, b.key))) }))] }) }));
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 };
185
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
+ }
265
+ /**
266
+ * Every project's issues, read the way GitHub reads them: open or closed, one
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.
271
+ */
186
272
  function Issues() {
187
273
  const host = useHost();
188
274
  const t = useTarget();
189
275
  const signed = Boolean(host.person);
190
276
  const [board, setBoard] = useKept(boardKey(host.org), '');
191
- const list = useRead(signed ? () => issues(t, board) : null, [], [t, signed, board]);
277
+ const [state, setState] = useState('open');
278
+ const [kind, setKind] = useState('all');
279
+ const [origin, setOrigin] = useState('all');
280
+ const [order, setOrder] = useState('newest');
281
+ const [label, setLabel] = useState('');
282
+ const [assignee, setAssignee] = useState('');
283
+ const [q, setQ] = useState('');
284
+ const [pulled, setPulled] = useState(0);
285
+ const [busy, setBusy] = useState(false);
286
+ const [said, setSaid] = useState(null);
287
+ const list = useRead(signed ? () => everything(t) : null, [], [t, signed, pulled]);
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);
192
294
  // Another screen chose the board (the rail's Issues chooses every board) while this one is open.
193
295
  useEffect(() => {
194
296
  const on = (e) => setBoard(e.detail);
195
297
  window.addEventListener('hanzo-board', on);
196
298
  return () => window.removeEventListener('hanzo-board', on);
197
299
  }, [setBoard]);
198
- const open = (w) => {
199
- const name = w.repo || w.project;
200
- if (name) {
201
- pinCodebase(host.org, {
202
- org: host.org ?? '',
203
- name,
204
- description: '',
205
- branch: 'main',
206
- public: false,
207
- clone: '',
208
- updated: '',
209
- branches: ['main'],
210
- }, `${w.identifier ? `${w.identifier} ` : ''}${w.title}`.trim());
300
+ useEffect(() => setSaid(null), [board]);
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();
303
+ for (const b of known.value)
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 });
309
+ }
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;
314
+ const title = current?.name ?? board;
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));
324
+ const shut = scoped.filter(closed);
325
+ const shown = (state === 'open' ? opened : shut).slice().sort((a, b) => {
326
+ if (order === 'updated')
327
+ return ms(b.updated || b.created) - ms(a.updated || a.created);
328
+ if (order === 'oldest')
329
+ return ms(a.created) - ms(b.created);
330
+ return ms(b.created) - ms(a.created);
331
+ });
332
+ const narrowed = q.trim() !== '' || kind !== 'all' || origin !== 'all' || label !== '' || assignee !== '';
333
+ const pull = async () => {
334
+ setBusy(true);
335
+ setSaid(null);
336
+ try {
337
+ const s = await syncIssues(t, board);
338
+ const where = board ? title : `${s.repos} ${s.repos === 1 ? 'repository' : 'repositories'}`;
339
+ const parts = [`${s.issues} ${s.issues === 1 ? 'issue' : 'issues'} synced from ${where}`, `${s.created} new`, `${s.updated} updated`];
340
+ if (s.failed)
341
+ parts.push(`${s.failed} failed`);
342
+ setSaid({ text: `${parts.join(' · ')}.${s.truncated ? ' More remain — sync again to continue.' : ''}` });
343
+ setPulled((n) => n + 1);
211
344
  }
212
- host.go('');
345
+ catch (e) {
346
+ const text = e.message;
347
+ setSaid({ text, connect: /not connected/i.test(text) });
348
+ }
349
+ finally {
350
+ setBusy(false);
351
+ }
352
+ };
353
+ const link = async () => {
354
+ try {
355
+ host.open(await connect(t, here()));
356
+ }
357
+ catch (e) {
358
+ setSaid({ text: e.message });
359
+ }
360
+ };
361
+ const chip = (key, name, count) => {
362
+ const on = key.toLowerCase() === board.toLowerCase();
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 || '*'));
213
364
  };
214
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Issues", says: board ? `Open work on ${board}. Choosing one starts the next run on that codebase.` : 'Open work across every board. Choosing one starts the next run on that codebase.' }), board ? (_jsx(XStack, { render: "button", onPress: () => setBoard(''), "aria-label": "Show every board", self: "flex-start", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "All boards" }) })) : null, 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" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "Nothing open." }), _jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })), children: "View repositories" }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "Start a new run" })] })] })) : (_jsx(YStack, { gap: "$2", children: list.value.map((w) => (_jsxs(Row, { label: `Build ${w.identifier || w.title}`, onPress: () => open(w), children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 88, numberOfLines: 1, children: w.identifier || w.kind }), _jsx(SizableText, { size: "$3", color: "$ink", flex: 1, minW: 0, numberOfLines: 1, children: w.title }), _jsx(SizableText, { size: "$1", color: "$soft", children: STATUS[w.status] ?? w.status })] }, 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] }));
215
378
  }
package/lib/host.d.ts CHANGED
@@ -39,7 +39,7 @@ export interface Host {
39
39
  person: Person | null;
40
40
  /** Whether the person administers `org` (its owner or an admin): they publish to main and change its money; anyone else opens a review and reads. */
41
41
  admin: boolean;
42
- /** The address under the mount: '' | a session id | a slug | '-/codebases' | '-/projects' | '-/issues' | '-/artifacts' | '-/templates'. */
42
+ /** The address under the mount, in route.ts's grammar: '' | a session id | `<org>/<repo>` | a slug | '-/projects' | '-/issues' | … */
43
43
  path: string;
44
44
  /** Move under the mount. */
45
45
  go: (path: string, how?: {
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
  *