@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/README.md +7 -7
- package/lib/api/github.d.ts +18 -0
- package/lib/api/github.js +19 -0
- package/lib/api/keys.d.ts +21 -13
- package/lib/api/keys.js +35 -19
- package/lib/api/work.d.ts +14 -0
- package/lib/api/work.js +17 -0
- package/lib/builder.js +17 -10
- package/lib/forge.js +175 -80
- package/lib/host.d.ts +1 -1
- package/lib/landing.js +9 -9
- package/lib/project.d.ts +16 -1
- package/lib/project.js +86 -28
- package/lib/route.d.ts +12 -4
- package/lib/route.js +8 -1
- package/lib/section.d.ts +1 -1
- package/lib/section.js +3 -3
- package/lib/settings/keys.js +22 -27
- package/lib/sync.js +2 -2
- package/package.json +1 -1
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
|
-
*
|
|
3
|
+
* Projects and issues, read from the forge and opened in this window.
|
|
4
4
|
*
|
|
5
|
-
* A
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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,
|
|
11
|
-
import { Button,
|
|
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
|
|
16
|
+
import { codebases } from './api/codebases.js';
|
|
15
17
|
import { Automations } from './automations.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
69
|
-
const list = useRead(signed ? () => codebases(t) : null, [], [t, signed,
|
|
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 (
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
};
|
|
139
|
-
return (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
153
|
+
setNote('');
|
|
161
154
|
try {
|
|
162
|
-
await
|
|
163
|
-
setName('');
|
|
164
|
-
setDescription('');
|
|
155
|
+
host.open(await connect(t, here()));
|
|
165
156
|
}
|
|
166
157
|
catch (e) {
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
158
|
+
setNote(e.message);
|
|
170
159
|
setBusy(false);
|
|
171
160
|
}
|
|
172
161
|
};
|
|
173
|
-
return (
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
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 | '-/
|
|
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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
148
|
-
setNote(
|
|
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: "
|
|
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
|
-
|
|
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;
|