@hanzo/build 0.2.50 → 0.2.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/forge.js CHANGED
@@ -1,33 +1,30 @@
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, 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.
8
10
  */
9
11
  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 { 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';
12
14
  import { useEffect, useState } from 'react';
13
15
  import { ago } from './ago.js';
14
- import { codebases, create } from './api/codebases.js';
16
+ import { codebases } from './api/codebases.js';
15
17
  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';
18
+ import { boards, closed, inProject, issues, projectOf } from './api/work.js';
19
+ import { boardKey, pinCodebase, readPending, writePending } from './choice.js';
19
20
  import { useKept, useRead } from './data.js';
20
21
  import { useHost, useTarget } from './host.js';
21
22
  import { path } from './route.js';
22
23
  import { Customize } from './customize/index.js';
23
24
  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
- }
25
+ import { connect, connection, grants, syncIssues } from './api/github.js';
26
+ import { here, useBack } from './back.js';
27
+ import { Out } from './out.js';
31
28
  function Note({ children }) {
32
29
  return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
33
30
  }
@@ -46,34 +43,34 @@ export function Forge({ screen }) {
46
43
  // The fleet's native servers are Connectors → Discover now; the old address lands there.
47
44
  if (screen === 'mcp')
48
45
  return _jsx(Customize, { tab: "connectors", view: "discover" });
49
- if (screen === 'codebases')
50
- return _jsx(Codebases, {});
51
46
  if (screen === 'projects')
52
47
  return _jsx(Projects, {});
53
48
  return _jsx(Issues, {});
54
49
  }
55
50
  const PAGE = 25;
56
- function Codebases() {
51
+ /**
52
+ * The organization's projects: its repositories on the forge, which arrive one
53
+ * of two ways — linked from GitHub, or started from a template. A project is a
54
+ * repository; opening one opens its workspace at `<org>/<repo>`, and every run
55
+ * there works on that repository. There is no project without one.
56
+ */
57
+ function Projects() {
57
58
  const host = useHost();
58
59
  const t = useTarget();
59
60
  const signed = Boolean(host.person);
60
61
  const [q, setQ] = useState('');
61
- const [making, setMaking] = useState(false);
62
- const [sort, setSort] = useState('name');
63
- const [filter, setFilter] = useState('all');
62
+ const [sort, setSort] = useState('updated');
64
63
  const [page, setPage] = useState(0);
65
64
  const [landed, setLanded] = useState(0);
66
65
  useEffect(() => {
67
66
  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]));
67
+ }, [q, sort]);
68
+ const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, landed]);
73
69
  const needle = q.trim().toLowerCase();
74
70
  const pending = readPending(host.org);
75
71
  const pendingKey = pending.map((p) => p.fullName).join('\n');
76
72
  const syncing = new Set(pending.map((p) => p.name));
73
+ // A repository asked for from GitHub is listed as syncing until the forge holds it.
77
74
  useEffect(() => {
78
75
  const waiting = readPending(host.org);
79
76
  if (!signed || waiting.length === 0)
@@ -110,19 +107,12 @@ function Codebases() {
110
107
  branches: ['main'],
111
108
  })),
112
109
  ]
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
110
  .filter((c) => !needle || `${c.org}/${c.name} ${c.description}`.toLowerCase().includes(needle))
121
111
  .slice()
122
112
  .sort((a, b) => {
123
113
  if (sort === 'updated') {
124
- const d = Date.parse(b.updated) - Date.parse(a.updated);
125
- if (Number.isFinite(d) && d !== 0)
114
+ const d = (Date.parse(b.updated) || 0) - (Date.parse(a.updated) || 0);
115
+ if (d !== 0)
126
116
  return d;
127
117
  }
128
118
  return a.name.localeCompare(b.name);
@@ -132,69 +122,158 @@ function Codebases() {
132
122
  const slice = shown.slice(safePage * PAGE, safePage * PAGE + PAGE);
133
123
  const from = shown.length === 0 ? 0 : safePage * PAGE + 1;
134
124
  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
- } })] }));
125
+ const none = !list.loading && !list.error && list.value.length === 0 && pending.length === 0;
126
+ const open = (c) => host.go(path({ kind: 'repo', org: c.org || host.org || '', name: c.name }));
127
+ const link = () => host.go(path({ kind: 'screen', screen: 'sync' }));
128
+ const template = () => host.go(path({ kind: 'screen', screen: 'templates' }));
129
+ 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" }) })] })] })] }))] }))] }) }));
130
+ }
131
+ /** One of the two ways in, as a card you press. */
132
+ function Choice({ label, icon, says, onPress }) {
133
+ 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 })] }));
145
134
  }
146
- function NewRepo({ open, onOpenChange, onCreate, }) {
147
- const [name, setName] = useState('');
148
- const [description, setDescription] = useState('');
135
+ /**
136
+ * The person's GitHub connection, in one line: who it is connected as and the
137
+ * way to sync, or the one press that connects it. GitHub returns here, and this
138
+ * page finishes the connect (../back.ts).
139
+ */
140
+ function GitHubLink({ onSync }) {
141
+ const host = useHost();
142
+ const t = useTarget();
143
+ const me = useRead(() => connection(t), null, [t]);
144
+ const [note, setNote] = useState('');
149
145
  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 () => {
146
+ useBack(t, true, setNote, me.reload);
147
+ const c = me.value;
148
+ // A connection that cannot be read says nothing here: Sync from GitHub above still works.
149
+ if (me.error || !c || !c.configured)
150
+ return note ? _jsx(Note, { children: note }) : null;
151
+ const link = async () => {
159
152
  setBusy(true);
160
- setError('');
153
+ setNote('');
161
154
  try {
162
- await onCreate(name, description);
163
- setName('');
164
- setDescription('');
155
+ host.open(await connect(t, here()));
165
156
  }
166
157
  catch (e) {
167
- setError(e.message);
168
- }
169
- finally {
158
+ setNote(e.message);
170
159
  setBusy(false);
171
160
  }
172
161
  };
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' })] })] })] }) }));
162
+ 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
163
  }
175
- function Projects() {
176
- const host = useHost();
177
- 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' }));
183
- };
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))) }))] }) }));
164
+ const KINDS = { all: 'Issues and pull requests', issue: 'Issues', pr: 'Pull requests' };
165
+ const ORIGINS = { all: 'Every source', github: 'GitHub', hanzo: 'Hanzo' };
166
+ 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);
169
+ /** A dropdown that picks one of `options`, its trigger naming the choice. */
170
+ function Pick({ label, value, options, onPick }) {
171
+ 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,
173
+ label: options[k],
174
+ icon: k === value ? _jsx(Check, { size: 14 }) : _jsx(YStack, { width: 14 }),
175
+ onSelect: () => onPick(k),
176
+ })) }));
177
+ }
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')
182
+ return _jsx(CircleSlash, { size: 16, color: "$soft" });
183
+ if (w.status === 'done')
184
+ return _jsx(CircleCheck, { size: 16, color: "$soft" });
185
+ return _jsx(CircleDot, { size: 16, color: "$ink" });
185
186
  }
187
+ function Tag({ children }) {
188
+ 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
+ }
190
+ /**
191
+ * 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.
195
+ */
186
196
  function Issues() {
187
197
  const host = useHost();
188
198
  const t = useTarget();
189
199
  const signed = Boolean(host.person);
190
200
  const [board, setBoard] = useKept(boardKey(host.org), '');
191
- const list = useRead(signed ? () => issues(t, board) : null, [], [t, signed, board]);
201
+ const [state, setState] = useState('open');
202
+ const [kind, setKind] = useState('all');
203
+ const [origin, setOrigin] = useState('all');
204
+ const [order, setOrder] = useState('newest');
205
+ const [q, setQ] = useState('');
206
+ const [pulled, setPulled] = useState(0);
207
+ const [busy, setBusy] = useState(false);
208
+ const [said, setSaid] = useState(null);
209
+ const list = useRead(signed ? () => issues(t) : null, [], [t, signed, pulled]);
210
+ const known = useRead(signed ? () => boards(t) : null, [], [t, signed]);
192
211
  // Another screen chose the board (the rail's Issues chooses every board) while this one is open.
193
212
  useEffect(() => {
194
213
  const on = (e) => setBoard(e.detail);
195
214
  window.addEventListener('hanzo-board', on);
196
215
  return () => window.removeEventListener('hanzo-board', on);
197
216
  }, [setBoard]);
217
+ 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();
220
+ 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);
230
+ }
231
+ const rows = [...projects.values()].sort((a, b) => a.name.localeCompare(b.name));
232
+ const current = board ? projects.get(board.toLowerCase()) : undefined;
233
+ 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));
241
+ const shut = scoped.filter(closed);
242
+ const shown = (state === 'open' ? opened : shut).slice().sort((a, b) => {
243
+ if (order === 'updated')
244
+ return (b.updated || b.created) - (a.updated || a.created);
245
+ if (order === 'oldest')
246
+ return a.created - b.created;
247
+ return b.created - a.created;
248
+ });
249
+ const pull = async () => {
250
+ setBusy(true);
251
+ setSaid(null);
252
+ try {
253
+ const s = await syncIssues(t, board);
254
+ const where = board ? title : `${s.repos} ${s.repos === 1 ? 'repository' : 'repositories'}`;
255
+ const parts = [`${s.issues} ${s.issues === 1 ? 'issue' : 'issues'} synced from ${where}`, `${s.created} new`, `${s.updated} updated`];
256
+ if (s.failed)
257
+ parts.push(`${s.failed} failed`);
258
+ setSaid({ text: `${parts.join(' · ')}.${s.truncated ? ' More remain — sync again to continue.' : ''}` });
259
+ setPulled((n) => n + 1);
260
+ }
261
+ catch (e) {
262
+ const text = e.message;
263
+ setSaid({ text, connect: /not connected/i.test(text) });
264
+ }
265
+ finally {
266
+ setBusy(false);
267
+ }
268
+ };
269
+ const link = async () => {
270
+ try {
271
+ host.open(await connect(t, here()));
272
+ }
273
+ catch (e) {
274
+ setSaid({ text: e.message });
275
+ }
276
+ };
198
277
  const open = (w) => {
199
278
  const name = w.repo || w.project;
200
279
  if (name) {
@@ -211,5 +290,21 @@ function Issues() {
211
290
  }
212
291
  host.go('');
213
292
  };
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))) }))] }) }));
293
+ const chip = (key, name, count) => {
294
+ const on = key.toLowerCase() === board.toLowerCase();
295
+ 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
+ };
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))))] }))] })] })] }) }));
215
310
  }
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/landing.js CHANGED
@@ -6,11 +6,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * which repository and branch it starts from, and the ask itself.
7
7
  * Under it: attach, dictate, the mode, and the model and effort on the right.
8
8
  *
9
- * Sending starts a coding run and opens it. The codebase is optional: in the
10
- * sandbox a run with none starts a new project named from the ask, as the
11
- * platform does for a run that names no repository; a machine works in its own
12
- * checkout, so a run there names one. The choices a person made here are kept in
13
- * this browser, per org, so the next New starts where the last one did.
9
+ * Sending starts a coding run on a project and opens it. A project is a
10
+ * repository — the org's on the forge, or one its GitHub grants — so a run
11
+ * names one, and an ask with none chosen opens the picker rather than making a
12
+ * project up from its words. The choices a person made here are kept in this
13
+ * browser, per org, so the next New starts where the last one did.
14
14
  */
15
15
  import { SizableText, XStack, YStack } from '@hanzo/gui';
16
16
  import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
@@ -144,8 +144,9 @@ export function Landing({ onStarted }) {
144
144
  }
145
145
  const repo = isForge(kept.repo) ? kept.repo : null;
146
146
  const hub = isHub(kept.repo) ? kept.repo : null;
147
- if (!repo && !hub && place.id) {
148
- setNote(`A run on ${place.label} works in its own checkout: choose the codebase, or run it in Cloud.`);
147
+ if (!repo && !hub) {
148
+ setNote('Choose the project this run works on: a repository of this organization, or one on its GitHub.');
149
+ setPicking(true);
149
150
  return;
150
151
  }
151
152
  setBusy(true);
@@ -154,7 +155,6 @@ export function Landing({ onStarted }) {
154
155
  const run = await start(t, {
155
156
  prompt: compose(draft.trim(), files),
156
157
  // The name alone. The org rides the request, and a slash is not a repo name.
157
- // No codebase is not sent, and the run starts a new project.
158
158
  // An empty base or place is not sent: the default branch, and the sandbox.
159
159
  repo: repo?.name ?? hub?.full_name,
160
160
  base: repo ? kept.branch : hub ? kept.branch || hub.default_branch : undefined,
@@ -227,7 +227,7 @@ export function Landing({ onStarted }) {
227
227
  name: r.name,
228
228
  });
229
229
  },
230
- }, placeholder: "New project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
230
+ }, placeholder: "Choose a project", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
231
231
  const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ModelPicker, { quiet: true, size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: model, onChange: pickModel, loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
232
232
  return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", px: "$4", pb: "$10", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, gap: "$5", children: [_jsxs(XStack, { role: "heading", "aria-level": 1, justify: "center", items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(HanzoMark, { size: 26 }), _jsx(SizableText, { size: "$8", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' })] }), _jsxs(YStack, { gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
233
233
  ? `A proposed environment for ${codebase} is waiting for review.`
package/lib/project.d.ts CHANGED
@@ -1,3 +1,18 @@
1
- export declare function Project({ slug }: {
1
+ import { type Project as Row } from './api/projects.ts';
2
+ /** What a workspace is opened on: a repository on the forge, or a deployed site by its slug. */
3
+ export type Where = {
2
4
  slug: string;
5
+ } | {
6
+ org: string;
7
+ name: string;
8
+ };
9
+ /** Whether the site `p` is the repository `r`'s: built from it, or named as it is. */
10
+ export declare function siteOf(p: Row, r: {
11
+ org: string;
12
+ name: string;
13
+ }): boolean;
14
+ /** A site's slug for a repository's name: lowercase letters, digits and hyphens, at most 40. */
15
+ export declare function slugOf(name: string): string;
16
+ export declare function Project({ at }: {
17
+ at: Where;
3
18
  }): import("react").JSX.Element;