@hanzo/build 0.2.51 → 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 +6 -6
- package/lib/api/github.d.ts +18 -0
- package/lib/api/github.js +19 -0
- 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/sync.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,18 +99,18 @@ hanzo.id token for `Host.admin`. One left column, never two.
|
|
|
99
99
|
| `-/automations` | repeating work, read from `/v1/auto/flows` |
|
|
100
100
|
| `-/customize` · `-/customize/connectors` · `/plugins` · `/agents` | Customize: skills, connectors, plugins, agents |
|
|
101
101
|
| `-/mcp` | Connectors → Discover, where the fleet's native MCP servers now live |
|
|
102
|
-
| `-/
|
|
103
|
-
| `-/sync` |
|
|
104
|
-
| `-/projects` | the forge's boards |
|
|
102
|
+
| `-/projects` | the org's projects: its repositories on the forge |
|
|
103
|
+
| `-/sync` | link repositories from GitHub onto the forge |
|
|
105
104
|
| `-/issues` | the forge's issues |
|
|
106
105
|
| `-/artifacts` | what the org has built |
|
|
107
106
|
| `-/templates` | the public starters |
|
|
108
107
|
| `-/plans` | the plans this brand sells, and the move to another |
|
|
109
108
|
| `-/settings/<section>` | Billing, Usage, Members, Integrations, Notifications, … |
|
|
110
|
-
| `<
|
|
109
|
+
| `<org>/<repo>` | a project's workspace: the repository, and its site when it has one |
|
|
110
|
+
| `<slug>` | a deployed site's workspace |
|
|
111
111
|
|
|
112
|
-
A project slug holds no `_` and never starts with `-`,
|
|
113
|
-
collide.
|
|
112
|
+
A project slug holds no `_` and never starts with `-`, and a repository is the
|
|
113
|
+
one address of two segments, so the forms cannot collide.
|
|
114
114
|
|
|
115
115
|
## What is on the screen
|
|
116
116
|
|
package/lib/api/github.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* POST /v1/provider/github/user/complete {grant} → the connection
|
|
9
9
|
* POST /v1/provider/github/user/disconnect
|
|
10
10
|
* GET /v1/provider/github/installations the accounts this org has bound the App on
|
|
11
|
+
* POST /v1/provider/github/issues/backfill {state, repo?} → mirror GitHub issues onto the board
|
|
11
12
|
*
|
|
12
13
|
* Paged by an opaque `after` cursor; `next` is the cursor for the page after
|
|
13
14
|
* this one, or empty on the last. Rows are read defensively: a field the
|
|
@@ -128,3 +129,20 @@ export declare function accounts(g: Grants): Account[];
|
|
|
128
129
|
export declare function grants(t: Target, signal?: AbortSignal): Promise<Grants>;
|
|
129
130
|
/** Queue a mirror of the named repositories onto the forge. The names are `owner/name`. */
|
|
130
131
|
export declare function bring(t: Target, repos: string[]): Promise<number>;
|
|
132
|
+
/** What one issue sync did. */
|
|
133
|
+
export interface Synced {
|
|
134
|
+
repos: number;
|
|
135
|
+
issues: number;
|
|
136
|
+
created: number;
|
|
137
|
+
updated: number;
|
|
138
|
+
failed: number;
|
|
139
|
+
/** The pass stopped at its budget; syncing again continues where it stopped. */
|
|
140
|
+
truncated: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Mirror GitHub issues onto the board — open and closed, so a closed one reads as
|
|
144
|
+
* closed here too. `repo` (`name` or `owner/name`) syncs one project; empty syncs
|
|
145
|
+
* every repository the connection grants. Idempotent: a second sync updates and
|
|
146
|
+
* never duplicates, and the App's webhook keeps them live afterwards.
|
|
147
|
+
*/
|
|
148
|
+
export declare function syncIssues(t: Target, repo?: string): Promise<Synced>;
|
package/lib/api/github.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* POST /v1/provider/github/user/complete {grant} → the connection
|
|
9
9
|
* POST /v1/provider/github/user/disconnect
|
|
10
10
|
* GET /v1/provider/github/installations the accounts this org has bound the App on
|
|
11
|
+
* POST /v1/provider/github/issues/backfill {state, repo?} → mirror GitHub issues onto the board
|
|
11
12
|
*
|
|
12
13
|
* Paged by an opaque `after` cursor; `next` is the cursor for the page after
|
|
13
14
|
* this one, or empty on the last. Rows are read defensively: a field the
|
|
@@ -143,3 +144,21 @@ export async function bring(t, repos) {
|
|
|
143
144
|
const raw = obj(await call(t, 'POST', '/v1/provider/github/repos/import', { repos: names }));
|
|
144
145
|
return num(raw.queued);
|
|
145
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Mirror GitHub issues onto the board — open and closed, so a closed one reads as
|
|
149
|
+
* closed here too. `repo` (`name` or `owner/name`) syncs one project; empty syncs
|
|
150
|
+
* every repository the connection grants. Idempotent: a second sync updates and
|
|
151
|
+
* never duplicates, and the App's webhook keeps them live afterwards.
|
|
152
|
+
*/
|
|
153
|
+
export async function syncIssues(t, repo = '') {
|
|
154
|
+
const name = repo.trim();
|
|
155
|
+
const raw = obj(await call(t, 'POST', '/v1/provider/github/issues/backfill', name ? { state: 'all', repo: name } : { state: 'all' }));
|
|
156
|
+
return {
|
|
157
|
+
repos: num(raw.repos),
|
|
158
|
+
issues: num(raw.issues),
|
|
159
|
+
created: num(raw.created),
|
|
160
|
+
updated: num(raw.updated),
|
|
161
|
+
failed: num(raw.failed),
|
|
162
|
+
truncated: raw.truncated === true,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/lib/api/work.d.ts
CHANGED
|
@@ -31,7 +31,21 @@ export interface Work {
|
|
|
31
31
|
assignee: string;
|
|
32
32
|
/** The repository the item is bound to, or ''. */
|
|
33
33
|
repo: string;
|
|
34
|
+
/** Where it was opened: `git` (the forge, or mirrored from GitHub), `team`, `agent`, … */
|
|
35
|
+
source: string;
|
|
36
|
+
labels: string[];
|
|
37
|
+
/** Its page upstream (a GitHub issue for a mirrored one), https only, or ''. */
|
|
38
|
+
url: string;
|
|
39
|
+
/** Unix milliseconds, or 0 when the platform did not say. */
|
|
40
|
+
created: number;
|
|
41
|
+
updated: number;
|
|
34
42
|
}
|
|
43
|
+
/** The project an item is filed under: its repository, else its board. */
|
|
44
|
+
export declare const projectOf: (w: Work) => string;
|
|
45
|
+
/** Done and canceled are closed; every other column is open work. */
|
|
46
|
+
export declare const closed: (w: Work) => boolean;
|
|
47
|
+
/** Whether `w` belongs to the project `key` — by repository or by board, either case. */
|
|
48
|
+
export declare function inProject(w: Work, key: string): boolean;
|
|
35
49
|
export declare function board(raw: unknown): Board | null;
|
|
36
50
|
export declare function work(raw: unknown): Work | null;
|
|
37
51
|
export declare function boards(t: Target): Promise<Board[]>;
|
package/lib/api/work.js
CHANGED
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
* reads the work and opens a run against it.
|
|
13
13
|
*/
|
|
14
14
|
import { call, seg } from './call.js';
|
|
15
|
+
/** The project an item is filed under: its repository, else its board. */
|
|
16
|
+
export const projectOf = (w) => w.repo || w.project;
|
|
17
|
+
/** Done and canceled are closed; every other column is open work. */
|
|
18
|
+
export const closed = (w) => w.status === 'done' || w.status === 'canceled';
|
|
19
|
+
/** Whether `w` belongs to the project `key` — by repository or by board, either case. */
|
|
20
|
+
export function inProject(w, key) {
|
|
21
|
+
const k = key.toLowerCase();
|
|
22
|
+
return w.repo.toLowerCase() === k || w.project.toLowerCase() === k;
|
|
23
|
+
}
|
|
15
24
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
25
|
+
/** The platform states seconds; a value already in milliseconds is kept. */
|
|
26
|
+
const when = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? (v < 1e12 ? v * 1000 : v) : 0);
|
|
27
|
+
const https = (v) => (typeof v === 'string' && /^https:\/\//.test(v) ? v : '');
|
|
16
28
|
const obj = (v) => v && typeof v === 'object' ? v : {};
|
|
17
29
|
function rows(raw) {
|
|
18
30
|
if (Array.isArray(raw))
|
|
@@ -49,6 +61,11 @@ export function work(raw) {
|
|
|
49
61
|
priority: str(o.priority) || 'none',
|
|
50
62
|
assignee: str(o.assignee),
|
|
51
63
|
repo: str(o.repo),
|
|
64
|
+
source: str(o.source),
|
|
65
|
+
labels: Array.isArray(o.labels) ? o.labels.filter((l) => typeof l === 'string' && l !== '') : [],
|
|
66
|
+
url: https(o.url) || https(o.extRef),
|
|
67
|
+
created: when(o.createdAt),
|
|
68
|
+
updated: when(o.updatedAt),
|
|
52
69
|
};
|
|
53
70
|
}
|
|
54
71
|
export async function boards(t) {
|
package/lib/builder.js
CHANGED
|
@@ -5,14 +5,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* / New — the empty state, a composer over the codebase it works on
|
|
6
6
|
* /sess_… one run, live
|
|
7
7
|
* /-/automations work that runs itself; /-/automations/<id> one, to edit and run
|
|
8
|
-
/-/
|
|
9
|
-
* /-/sync
|
|
10
|
-
* /-/projects the forge's boards
|
|
8
|
+
* /-/projects the org's repositories on the forge: a project is a repository
|
|
9
|
+
* /-/sync link repositories from GitHub onto the forge
|
|
11
10
|
* /-/issues the forge's issues
|
|
12
11
|
* /-/artifacts what this org has built
|
|
13
12
|
* /-/templates the public starters
|
|
14
13
|
* /-/customize skills, connectors, plugins and agents
|
|
15
|
-
* /<
|
|
14
|
+
* /<org>/<repo> a repository's workspace, in this same window
|
|
15
|
+
* /<slug> a deployed site's workspace
|
|
16
16
|
*
|
|
17
17
|
* The rail is the sessions rail every Hanzo surface shares: New, the builder's
|
|
18
18
|
* places with the rest under More, the org's coding runs newest first with a
|
|
@@ -20,15 +20,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
* whose menu opens Settings, Usage, the plans, help and logging out (foot.tsx).
|
|
21
21
|
* Its collapse is an explicit toggle kept per browser.
|
|
22
22
|
*
|
|
23
|
-
* A
|
|
24
|
-
*
|
|
23
|
+
* A workspace takes the whole window — it has its own chat column, bar and
|
|
24
|
+
* panes — and its mark leads back here.
|
|
25
25
|
*
|
|
26
26
|
* A host with a rail of its own mounts `<Builder rail={false}>` and draws the
|
|
27
27
|
* builder's places (`nav`) and runs in that rail (`useSessions`, or `DevSection`,
|
|
28
28
|
* section.tsx): one left column, never two.
|
|
29
29
|
*/
|
|
30
30
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
31
|
-
import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2,
|
|
31
|
+
import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, LayoutTemplate, Menu, Search, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
|
|
32
32
|
import { Button } from '@hanzo/ui';
|
|
33
33
|
import { SessionRail } from '@hanzo/ui/chat';
|
|
34
34
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
@@ -98,13 +98,18 @@ export function nav(host, go) {
|
|
|
98
98
|
const r = route(host.path);
|
|
99
99
|
const screen = r.kind === 'screen' ? r.screen : '';
|
|
100
100
|
const links = [
|
|
101
|
-
{
|
|
101
|
+
{
|
|
102
|
+
id: 'projects',
|
|
103
|
+
label: 'Projects',
|
|
104
|
+
icon: _jsx(FolderGit2, { size: 16 }),
|
|
105
|
+
onPress: () => go(path({ kind: 'screen', screen: 'projects' })),
|
|
106
|
+
active: screen === 'projects' || screen === 'sync' || r.kind === 'repo',
|
|
107
|
+
},
|
|
102
108
|
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
103
109
|
{ id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: () => go(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
|
|
104
110
|
{ id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' || r.kind === 'automation' },
|
|
105
111
|
];
|
|
106
112
|
const more = [
|
|
107
|
-
{ id: 'codebases', label: 'Codebase', icon: _jsx(FolderGit2, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), active: screen === 'codebases' },
|
|
108
113
|
{
|
|
109
114
|
id: 'issues',
|
|
110
115
|
label: 'Issues',
|
|
@@ -172,7 +177,9 @@ function Screens({ rail }) {
|
|
|
172
177
|
const host = useHost();
|
|
173
178
|
const r = route(host.path);
|
|
174
179
|
if (r.kind === 'project')
|
|
175
|
-
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
180
|
+
return _jsx(Project, { at: { slug: r.slug } }, r.slug);
|
|
181
|
+
if (r.kind === 'repo')
|
|
182
|
+
return _jsx(Project, { at: { org: r.org, name: r.name } }, `${r.org}/${r.name}`);
|
|
176
183
|
if (!rail)
|
|
177
184
|
return _jsx(Pane, {});
|
|
178
185
|
return _jsx(Shell, {});
|
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;
|
package/lib/project.js
CHANGED
|
@@ -3,6 +3,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
* A project's workspace, in the same window: the chat on the left, what it
|
|
4
4
|
* built on the right.
|
|
5
5
|
*
|
|
6
|
+
* A project is a repository on the forge, addressed `<org>/<repo>`; the site it
|
|
7
|
+
* publishes to, when it has one, is the org's project row on that repository
|
|
8
|
+
* (or of the same name). A deployed site with no repository on the forge is
|
|
9
|
+
* opened by its slug (Artifacts), and works the same way.
|
|
10
|
+
*
|
|
6
11
|
* bar mark · project · history · chat toggle | Preview Files Code Layers |
|
|
7
12
|
* Share · Publish
|
|
8
13
|
* left the project's runs as a conversation, suggestions, and the ask
|
|
@@ -41,10 +46,11 @@ import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
|
41
46
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
42
47
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
43
48
|
import { read as pushed } from './api/changes.js';
|
|
49
|
+
import { codebases, one } from './api/codebases.js';
|
|
44
50
|
import { start } from './api/coding.js';
|
|
45
51
|
import { blob, tree } from './api/git.js';
|
|
46
52
|
import { declare } from './api/platform.js';
|
|
47
|
-
import { deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
53
|
+
import { address, deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
48
54
|
import { list, message, stop } from './api/sessions.js';
|
|
49
55
|
import { decode, outcome, pull, said, who } from './api/turn.js';
|
|
50
56
|
import { verdict as record } from './api/verdict.js';
|
|
@@ -54,8 +60,8 @@ import { Grip } from './grip.js';
|
|
|
54
60
|
import { useHost, useTarget } from './host.js';
|
|
55
61
|
import { MODES } from './landing.js';
|
|
56
62
|
import { Out } from './out.js';
|
|
57
|
-
import { pane as cut } from './pane.js';
|
|
58
63
|
import { Publish } from './publish.js';
|
|
64
|
+
import { path } from './route.js';
|
|
59
65
|
import { Attach, compose, Dictate, Files } from './tools.js';
|
|
60
66
|
const LIVE = new Set(['running', 'paused']);
|
|
61
67
|
/** How often a project being published is read again, ms. */
|
|
@@ -70,6 +76,20 @@ const CHAT_CEIL = 640;
|
|
|
70
76
|
const GUTTER = 12;
|
|
71
77
|
/** What the work keeps beside the chat however wide the chat was asked to be, px. */
|
|
72
78
|
const WORK = 400 + 2 * GUTTER;
|
|
79
|
+
/** Whether the site `p` is the repository `r`'s: built from it, or named as it is. */
|
|
80
|
+
export function siteOf(p, r) {
|
|
81
|
+
const want = `${r.org}/${r.name}`.toLowerCase();
|
|
82
|
+
return (p.repo !== '' && address(p.repo).toLowerCase() === want) || p.slug === r.name.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
/** A site's slug for a repository's name: lowercase letters, digits and hyphens, at most 40. */
|
|
85
|
+
export function slugOf(name) {
|
|
86
|
+
return name
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
89
|
+
.replace(/^-+|-+$/g, '')
|
|
90
|
+
.slice(0, 40)
|
|
91
|
+
.replace(/-+$/, '');
|
|
92
|
+
}
|
|
73
93
|
/** One run in the conversation: what was asked, and what it came to. */
|
|
74
94
|
function Turn({ run, open, onOpen, project }) {
|
|
75
95
|
const t = useTarget();
|
|
@@ -83,12 +103,18 @@ function Turn({ run, open, onOpen, project }) {
|
|
|
83
103
|
});
|
|
84
104
|
} }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: note })) : null, !open ? (_jsx(XStack, { render: "button", onPress: onOpen, hitSlop: 8, children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Show steps" }) })) : null] }), children: _jsx(SizableText, { size: "$2", color: "$ink", children: run.status === 'done' ? 'Done.' : run.status === 'error' ? 'This run hit an error.' : run.status === 'stopped' ? 'Stopped.' : 'Working…' }) })] }));
|
|
85
105
|
}
|
|
86
|
-
export function Project({
|
|
106
|
+
export function Project({ at }) {
|
|
87
107
|
const host = useHost();
|
|
88
108
|
const t = useTarget();
|
|
89
109
|
const signed = Boolean(host.person);
|
|
90
110
|
const all = useProjects(t, signed);
|
|
91
|
-
|
|
111
|
+
// The repository the address names, and the forge's record of it.
|
|
112
|
+
const named = 'slug' in at ? null : at;
|
|
113
|
+
const forge = useRead(named && signed ? () => one(t, named.name) : null, null, [t, signed, named?.name]);
|
|
114
|
+
const project = all.value.find((p) => (named ? siteOf(p, named) : 'slug' in at && p.slug === at.slug)) ?? null;
|
|
115
|
+
// The site this workspace publishes to: the project's, or the repository's name as one.
|
|
116
|
+
const slug = project?.slug ?? ('slug' in at ? at.slug : slugOf(named?.name ?? ''));
|
|
117
|
+
const title = project?.name ?? named?.name ?? slug;
|
|
92
118
|
// A project that has never served reads `building` while it is published, and
|
|
93
119
|
// is read again until it is live or its build failed.
|
|
94
120
|
const building = project?.status === 'building';
|
|
@@ -113,12 +139,15 @@ export function Project({ slug }) {
|
|
|
113
139
|
const stalled = building && ran > STALL;
|
|
114
140
|
// Publish again: the project's repository, built at its branch, the way Publish builds it.
|
|
115
141
|
const [redo, setRedo] = useState({ busy: false, error: '' });
|
|
142
|
+
// The repository's clone address: the forge's for a repository, the site's own otherwise.
|
|
143
|
+
const clone = named ? (forge.value?.clone ?? '') : (project?.repo ?? '');
|
|
144
|
+
const home = named ? (forge.value?.branch ?? 'main') : project?.branch || 'main';
|
|
116
145
|
const retry = async () => {
|
|
117
|
-
if (!
|
|
146
|
+
if (!clone || redo.busy)
|
|
118
147
|
return;
|
|
119
148
|
setRedo({ busy: true, error: '' });
|
|
120
149
|
try {
|
|
121
|
-
await declare(t, { repo:
|
|
150
|
+
await declare(t, { repo: clone, ref: home, name: slug, project: slug, mode: 'branch' });
|
|
122
151
|
setNow(Date.now());
|
|
123
152
|
again();
|
|
124
153
|
setRedo({ busy: false, error: '' });
|
|
@@ -127,11 +156,14 @@ export function Project({ slug }) {
|
|
|
127
156
|
setRedo({ busy: false, error: e.message });
|
|
128
157
|
}
|
|
129
158
|
};
|
|
130
|
-
|
|
159
|
+
// The site's runs, or the org's when the repository has no site yet.
|
|
160
|
+
const sited = !named || Boolean(project);
|
|
161
|
+
const listed = useRead(signed ? () => list(t, sited ? { kind: 'coding', project: slug, limit: 50 } : { kind: 'coding', limit: 50 }) : null, [], [t, signed, slug, sited]);
|
|
131
162
|
// Only the runs on this project's own repository: a record can be moved into
|
|
132
163
|
// any project, and a run's branch is what Files, Code and Publish read.
|
|
133
|
-
const
|
|
134
|
-
const
|
|
164
|
+
const mine = (r) => (named ? ours(r.repo, `${named.org}/${named.name}`) && Boolean(r.repo) : ours(r.repo, project?.repo ?? ''));
|
|
165
|
+
const runs = { ...listed, value: listed.value.filter(mine) };
|
|
166
|
+
const ordered = useMemo(() => [...runs.value].reverse(), [listed.value, project?.repo, named?.org, named?.name]);
|
|
135
167
|
const [chosen, setChosen] = useState(null);
|
|
136
168
|
const current = chosen ?? runs.value[0]?.id ?? null;
|
|
137
169
|
const run = useRun(t, current);
|
|
@@ -149,6 +181,12 @@ export function Project({ slug }) {
|
|
|
149
181
|
// Below md the chat and the work take turns, and Chat is one more view to switch back to.
|
|
150
182
|
const [pane, setPane] = useState('chat');
|
|
151
183
|
const wide = useMedia().md;
|
|
184
|
+
// A workspace too narrow for the chat at its floor and the work at its room
|
|
185
|
+
// (a tablet beside the host's column) shows the work, and the chat when asked:
|
|
186
|
+
// never both cut. Asked here is for this visit; the kept choice is the wide one's.
|
|
187
|
+
const cramped = wide && room > 0 && room < CHAT_FLOOR + WORK;
|
|
188
|
+
const [asked, setAsked] = useState(false);
|
|
189
|
+
const shut = cramped ? !asked : collapsed;
|
|
152
190
|
const [dock, setDock] = useKept('hanzo.build.dock', 36);
|
|
153
191
|
const [dismissed, setDismissed] = useKept(`hanzo.build.hints.${slug}`, false);
|
|
154
192
|
const [mode, setMode] = useKept('hanzo.build.mode', 'build');
|
|
@@ -164,6 +202,7 @@ export function Project({ slug }) {
|
|
|
164
202
|
const [history, setHistory] = useState(false);
|
|
165
203
|
const [publish, setPublish] = useState(null);
|
|
166
204
|
const [switcher, setSwitcher] = useState(false);
|
|
205
|
+
const repos = useRead(switcher && signed ? () => codebases(t) : null, [], [t, switcher, signed]);
|
|
167
206
|
const frame = useRef(null);
|
|
168
207
|
// What was published may be new files at the same address: the project is read
|
|
169
208
|
// for its address, and the frame loads again.
|
|
@@ -196,8 +235,8 @@ export function Project({ slug }) {
|
|
|
196
235
|
// Files is drawn again after a merge, from what the forge holds now.
|
|
197
236
|
const [round, setRound] = useState(0);
|
|
198
237
|
// Files and Code: native git, at the branch the open run pushed (or the project's own).
|
|
199
|
-
const repo = project?.repo ? repoName(project.repo) : '';
|
|
200
|
-
const ref = run.record?.branch ||
|
|
238
|
+
const repo = named ? named.name : project?.repo ? repoName(project.repo) : '';
|
|
239
|
+
const ref = run.record?.branch || home;
|
|
201
240
|
const [files, setFiles] = useState([]);
|
|
202
241
|
const [file, setFile] = useState(null);
|
|
203
242
|
useEffect(() => {
|
|
@@ -294,9 +333,10 @@ export function Project({ slug }) {
|
|
|
294
333
|
// A project with no repository yet sends neither, and its first run makes one.
|
|
295
334
|
const next = await start(t, {
|
|
296
335
|
prompt: compose(`${ask}${context}`, attached),
|
|
297
|
-
|
|
336
|
+
// A repository with no site yet runs on the repository alone.
|
|
337
|
+
project: project?.slug,
|
|
298
338
|
repo,
|
|
299
|
-
base: project?.branch,
|
|
339
|
+
base: named ? forge.value?.branch : project?.branch,
|
|
300
340
|
after: current ?? undefined,
|
|
301
341
|
mode,
|
|
302
342
|
});
|
|
@@ -337,10 +377,21 @@ export function Project({ slug }) {
|
|
|
337
377
|
if (!signed) {
|
|
338
378
|
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", children: [_jsx(SizableText, { size: "$4", color: "$ink", children: "Sign in to open this project." }), _jsx(Button, { onPress: () => host.signIn?.(), children: "Sign in" })] }));
|
|
339
379
|
}
|
|
340
|
-
|
|
341
|
-
|
|
380
|
+
// A repository of another organization is opened in that one.
|
|
381
|
+
if (named && host.org && named.org.toLowerCase() !== host.org.toLowerCase()) {
|
|
382
|
+
const member = host.memberships?.some((m) => m.toLowerCase() === named.org.toLowerCase()) && host.chooseOrg;
|
|
383
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", text: "center", children: `${named.org}/${named.name} is in ${named.org}, and you are working in ${host.org}.` }), member ? (_jsx(Button, { variant: "outline", onPress: () => host.chooseOrg?.(named.org), children: `Switch to ${named.org}` })) : (_jsx(Button, { variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'projects' })), children: "See this organization\u2019s projects" }))] }));
|
|
384
|
+
}
|
|
385
|
+
if (named ? !forge.loading && !forge.value : !all.loading && !project && !all.error) {
|
|
386
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", text: "center", children: named
|
|
387
|
+
? forge.error && !/not found|404/i.test(forge.error.message)
|
|
388
|
+
? forge.error.message
|
|
389
|
+
: `${named.org} has no repository named ${named.name}.`
|
|
390
|
+
: host.org
|
|
391
|
+
? `${host.org} has no project named ${slug}.`
|
|
392
|
+
: `There is no project named ${slug}.` }), _jsx(Button, { variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: named ? 'projects' : 'artifacts' })), children: named ? 'See this organization’s projects' : 'See what this organization has built' })] }));
|
|
342
393
|
}
|
|
343
|
-
const chat = (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3",
|
|
394
|
+
const chat = (_jsxs(YStack, { "data-slot": "project-chat", flex: 1, minH: 0, position: "relative", children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", pt: "$2", pb: "$4", gap: "$4", $md: { pr: 0 }, children: ordered.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$ink", children: runs.loading
|
|
344
395
|
? 'Reading this project’s runs…'
|
|
345
396
|
: project?.live
|
|
346
397
|
? `${project.name} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.`
|
|
@@ -354,12 +405,14 @@ export function Project({ slug }) {
|
|
|
354
405
|
? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
|
|
355
406
|
: project
|
|
356
407
|
? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
|
|
357
|
-
:
|
|
408
|
+
: named
|
|
409
|
+
? `${named.org}/${named.name} is open on ${home}. Say what to change and it gets built there.`
|
|
410
|
+
: `${slug} is loaded. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { "data-slot": "project-ask", px: "$3", pb: "$3", gap: "$2", minW: 0, $md: { pr: 0 }, children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
|
|
358
411
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
359
|
-
: undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] }), wide && !
|
|
412
|
+
: undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] }), wide && !shut ? (_jsx(Grip, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", r: -GUTTER, width: GUTTER })) : null] }));
|
|
360
413
|
// The preview stays loaded while another view is open, so coming back to it
|
|
361
414
|
// is the page as it was left.
|
|
362
|
-
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${
|
|
415
|
+
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${title} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: stalled
|
|
363
416
|
? 'Publishing is taking too long'
|
|
364
417
|
: building
|
|
365
418
|
? project?.forked
|
|
@@ -373,29 +426,34 @@ export function Project({ slug }) {
|
|
|
373
426
|
? 'Its page appears here when the build finishes.'
|
|
374
427
|
: failed
|
|
375
428
|
? why
|
|
376
|
-
: 'Publish this project and its page appears here.' }), (stalled || failed) &&
|
|
429
|
+
: 'Publish this project and its page appears here.' }), (stalled || failed) && clone ? (_jsx(Button, { variant: "outline", disabled: redo.busy, onPress: () => void retry(), children: "Publish again" })) : null, redo.error ? (_jsx(SizableText, { size: "$2", color: "$soft", text: "center", role: "alert", children: redo.error })) : null] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
|
|
377
430
|
const next = !editing;
|
|
378
431
|
setEditing(next);
|
|
379
432
|
frame.current?.post({ type: 'preview:editable', active: next });
|
|
380
433
|
}, p: "$2", rounded: "$10", children: _jsx(MousePointerClick, { size: 16 }) })) : undefined })] }));
|
|
381
|
-
const body = view === 'preview' ? null : view === 'files' ? (repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` }, round)) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
|
|
434
|
+
const body = view === 'preview' ? null : view === 'files' ? (repo && root.error ? (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", role: "status", children: `Nothing to show for ${repo} at ${ref}: the forge answered “${root.error.message}”.` })) : repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` }, round)) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
|
|
382
435
|
setFiles((was) => was.filter((f) => f.path !== p));
|
|
383
436
|
if (file === p)
|
|
384
437
|
setFile(null);
|
|
385
438
|
}, empty: _jsxs(SizableText, { size: "$2", color: "$soft", p: "$4", children: ["Open a file from Files. Code here is read at ", ref, "; ask in the chat to change it."] }) })) : (_jsx(YStack, { p: "$4", gap: "$2", children: picked.length ? (picked.map((p) => (_jsxs(SizableText, { size: "$2", color: "$ink", children: [p.label, " \u2014 ", p.id] }, p.id)))) : (_jsx(SizableText, { size: "$2", color: "$soft", children: bridge
|
|
386
439
|
? 'Pick an element in the preview to see it here and attach it to your next ask.'
|
|
387
440
|
: 'Layers are read through the preview bridge, and this page does not run it. Its elements cannot be listed from here.' })) }));
|
|
388
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name:
|
|
441
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name: title, onPress: () => setSwitcher(true) }), _jsx(XStack, { render: "button", "aria-label": "History", onPress: () => setHistory(true), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Clock, { size: 16 }) }), _jsx(XStack, { render: "button", "aria-label": shut ? 'Show the chat' : 'Hide the chat', "aria-pressed": !shut, onPress: () => (cramped ? setAsked(!asked) : setCollapsed(!collapsed)), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, "$max-md": { display: 'none' }, children: _jsx(PanelLeft, { size: 16 }) })] }), middle: _jsx(Views, { views: [CHAT, ...VIEWS], value: pane === 'chat' && !wide ? 'chat' : view, onChange: (v) => {
|
|
389
442
|
if (v === 'chat')
|
|
390
443
|
return setPane('chat');
|
|
391
444
|
setView(v);
|
|
392
445
|
setPane('view');
|
|
393
|
-
}, label: "View" }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", disabled: !
|
|
394
|
-
setPublish({ repo: project.repo, title: project.name, ref, name: project.slug, project: project.slug }), children: [_jsx(Upload, { size: 14 }), " Publish"] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: collapsed, pane: pane, width: across, onLayout: (e) => setRoom(e.nativeEvent.layout.width), children: [preview, body] }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", project?.name ?? slug] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
446
|
+
}, label: "View" }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", "aria-label": "Publish", disabled: !clone || !slug, onPress: () => setPublish({ repo: clone, title, ref, name: slug, project: slug }), children: [_jsx(Upload, { size: 14 }), _jsx(SizableText, { size: "$2", color: "inherit", "$max-md": { display: 'none' }, children: "Publish" })] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: shut, pane: pane, width: across, onLayout: (e) => setRoom(e.nativeEvent.layout.width), children: [preview, body] }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", title] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
395
447
|
setChosen(r.id);
|
|
396
448
|
setHistory(false);
|
|
397
|
-
}, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
449
|
+
}, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children: repos.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: repos.error.message })) : repos.loading && repos.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the forge\u2026" })) : (repos.value.map((c) => {
|
|
450
|
+
const here = Boolean(named) && c.name === named?.name;
|
|
451
|
+
return (_jsx(XStack, { render: "button", "aria-label": `Open ${c.org}/${c.name}`, "aria-current": here ? 'page' : undefined, onPress: () => {
|
|
452
|
+
setSwitcher(false);
|
|
453
|
+
host.go(path({ kind: 'repo', org: c.org || host.org || '', name: c.name }));
|
|
454
|
+
}, px: "$2", py: "$1.5", rounded: "$3", bg: here ? '$hover' : undefined, hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: c.name }) }, `${c.org}/${c.name}`));
|
|
455
|
+
})) }), _jsx(Button, { variant: "outline", size: "sm", onPress: () => {
|
|
456
|
+
setSwitcher(false);
|
|
457
|
+
host.go(path({ kind: 'screen', screen: 'projects' }));
|
|
458
|
+
}, children: "All projects" })] }) }), _jsx(Publish, { source: publish, onClose: () => setPublish(null), onLive: fresh })] }));
|
|
401
459
|
}
|
package/lib/route.d.ts
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* -/settings[/<section>] a section of Settings
|
|
9
9
|
* -/customize[/<tab>] skills, connectors and plugins: what the agent brings to a run
|
|
10
10
|
* -/plans the plans an organization can be on
|
|
11
|
-
* <
|
|
11
|
+
* <org>/<repo> a repository's workspace: a project is a repository on the forge
|
|
12
|
+
* <slug> a deployed site's workspace (Artifacts)
|
|
12
13
|
*
|
|
13
14
|
* Unambiguous by construction: a project slug is `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`,
|
|
14
|
-
* so it can hold neither the `_` a session id carries nor start with
|
|
15
|
-
*
|
|
15
|
+
* so it can hold neither the `_` a session id carries nor start with `-`, and a
|
|
16
|
+
* repository is the one address with two segments, each starting with a letter
|
|
17
|
+
* or a digit. Anything else is not an address here and reads as the empty state.
|
|
16
18
|
*/
|
|
17
|
-
export type Screen = 'artifacts' | 'templates' | '
|
|
19
|
+
export type Screen = 'artifacts' | 'templates' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp' | 'plans';
|
|
18
20
|
/** Settings, one section each. Every setting the builder has lives on this page, none elsewhere. */
|
|
19
21
|
export declare const SECTIONS: readonly ['general', 'account', 'privacy', 'billing', 'usage', 'capabilities', 'memory', 'code', 'environments', 'machines', 'keys', 'members', 'integrations', 'notifications'];
|
|
20
22
|
export type Section = (typeof SECTIONS)[number];
|
|
@@ -38,12 +40,18 @@ export type Route = {
|
|
|
38
40
|
} | {
|
|
39
41
|
kind: 'project';
|
|
40
42
|
slug: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'repo';
|
|
45
|
+
org: string;
|
|
46
|
+
name: string;
|
|
41
47
|
} | {
|
|
42
48
|
kind: 'automation';
|
|
43
49
|
id: string;
|
|
44
50
|
};
|
|
45
51
|
export declare const SESSION: RegExp;
|
|
46
52
|
export declare const SLUG: RegExp;
|
|
53
|
+
/** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
|
|
54
|
+
export declare const NAME: RegExp;
|
|
47
55
|
/** An automation's id, or `new` for one not yet written. */
|
|
48
56
|
export declare const AUTOMATION: RegExp;
|
|
49
57
|
export declare function route(path: string): Route;
|
package/lib/route.js
CHANGED
|
@@ -19,9 +19,11 @@ export const SECTIONS = [
|
|
|
19
19
|
export const TABS = ['skills', 'connectors', 'plugins', 'agents'];
|
|
20
20
|
export const SESSION = /^sess_[0-9a-f]{32}$/;
|
|
21
21
|
export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
22
|
+
/** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
|
|
23
|
+
export const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
22
24
|
/** An automation's id, or `new` for one not yet written. */
|
|
23
25
|
export const AUTOMATION = /^(flow_[0-9a-f]{32}|new)$/;
|
|
24
|
-
const SCREENS = ['artifacts', 'templates', '
|
|
26
|
+
const SCREENS = ['artifacts', 'templates', 'projects', 'issues', 'automations', 'sync', 'mcp', 'plans'];
|
|
25
27
|
export function route(path) {
|
|
26
28
|
const p = path.replace(/^\/+|\/+$/g, '');
|
|
27
29
|
if (SESSION.test(p))
|
|
@@ -44,6 +46,9 @@ export function route(path) {
|
|
|
44
46
|
}
|
|
45
47
|
if (SLUG.test(p))
|
|
46
48
|
return { kind: 'project', slug: p };
|
|
49
|
+
const [org, name, ...rest] = p.split('/');
|
|
50
|
+
if (!rest.length && org && name && NAME.test(org) && NAME.test(name))
|
|
51
|
+
return { kind: 'repo', org, name };
|
|
47
52
|
return { kind: 'new' };
|
|
48
53
|
}
|
|
49
54
|
/** The path for a route — the inverse of `route`. */
|
|
@@ -61,6 +66,8 @@ export function path(r) {
|
|
|
61
66
|
return r.tab === 'skills' ? '-/customize' : `-/customize/${r.tab}`;
|
|
62
67
|
case 'project':
|
|
63
68
|
return r.slug;
|
|
69
|
+
case 'repo':
|
|
70
|
+
return `${r.org}/${r.name}`;
|
|
64
71
|
case 'automation':
|
|
65
72
|
return `-/automations/${r.id}`;
|
|
66
73
|
}
|
package/lib/section.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const DOTS: Record<string, SessionStatus>;
|
|
|
8
8
|
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
9
9
|
export declare function useSessions(host: Host): Read<Session[]>;
|
|
10
10
|
/**
|
|
11
|
-
* New, Automations, the forge (
|
|
11
|
+
* New, Automations, the forge (Projects, Issues), Artifacts, Templates, then the
|
|
12
12
|
* runs. `children` sit between the places and the runs — where a host lists its
|
|
13
13
|
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
14
14
|
* on a phone can close it.
|
package/lib/section.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* host's own `go`.
|
|
12
12
|
*/
|
|
13
13
|
import { SizableText } from '@hanzo/gui';
|
|
14
|
-
import { Blocks, CircleDot, FolderGit2,
|
|
14
|
+
import { Blocks, CircleDot, FolderGit2, LayoutTemplate, SlidersHorizontal, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
15
15
|
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
17
|
import { pinBoard } from './choice.js';
|
|
@@ -32,7 +32,7 @@ export function useSessions(host) {
|
|
|
32
32
|
}
|
|
33
33
|
const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1", children: children }));
|
|
34
34
|
/**
|
|
35
|
-
* New, Automations, the forge (
|
|
35
|
+
* New, Automations, the forge (Projects, Issues), Artifacts, Templates, then the
|
|
36
36
|
* runs. `children` sit between the places and the runs — where a host lists its
|
|
37
37
|
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
38
38
|
* on a phone can close it.
|
|
@@ -45,7 +45,7 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
|
45
45
|
onPick?.();
|
|
46
46
|
};
|
|
47
47
|
const screen = r.kind === 'screen' ? r.screen : '';
|
|
48
|
-
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations' || r.kind === 'automation', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === '
|
|
48
|
+
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations' || r.kind === 'automation', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === 'projects' || screen === 'sync' || r.kind === 'repo', onPress: () => go(path({ kind: 'screen', screen: 'projects' })), children: "Projects" }), _jsx(SidebarItem, { icon: _jsx(CircleDot, { size: 16, "aria-hidden": true }), active: screen === 'issues', onPress: () => {
|
|
49
49
|
pinBoard(host.org, '');
|
|
50
50
|
go(path({ kind: 'screen', screen: 'issues' }));
|
|
51
51
|
}, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(SlidersHorizontal, { size: 16, "aria-hidden": true }), active: r.kind === 'customize', onPress: () => go(path({ kind: 'customize', tab: 'connectors' })), children: "Customize" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
|
package/lib/sync.js
CHANGED
|
@@ -77,7 +77,7 @@ export function Sync() {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
const back = () => {
|
|
80
|
-
host.go(path({ kind: 'screen', screen: '
|
|
80
|
+
host.go(path({ kind: 'screen', screen: 'projects' }));
|
|
81
81
|
};
|
|
82
82
|
useEffect(() => {
|
|
83
83
|
if (!syncing)
|
|
@@ -102,7 +102,7 @@ export function Sync() {
|
|
|
102
102
|
window.clearInterval(id);
|
|
103
103
|
};
|
|
104
104
|
}, [syncing, t, host.org, host.go, picked]);
|
|
105
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to
|
|
105
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to projects", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Projects" }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: "/" }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Sync" })] }), _jsxs(YStack, { flex: 1, items: "center", justify: "center", px: "$6", pb: "$10", gap: "$5", children: [_jsxs(XStack, { justify: "center", gap: "$6", children: [_jsx(Step, { label: "Select repositories", done: step === 'access', current: step === 'pick' }), _jsx(Step, { label: "Repository access", done: false, current: step === 'access' })] }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$5", bg: "$panel", px: "$6", py: "$6", gap: "$4", width: "100%", maxW: 460, children: [step === 'pick' && !owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search organizations", "aria-label": "Search organizations", disabled: !signed }), signed ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${orgs.length} ${orgs.length === 1 ? 'organization' : 'organizations'}` })) : null, list.error ? _jsx(Soft, { children: list.error.message }) : null, !signed ? (_jsx(Soft, { children: "Sign in to choose repositories the GitHub connection can read." })) : list.loading && rows.length === 0 && orgs.length === 0 ? (_jsx(Soft, { children: "Reading the connection\u2026" })) : shownOrgs.length === 0 ? (_jsx(Soft, { children: orgs.length === 0 ? 'No organizations on this connection yet.' : 'Nothing matches.' })) : (shownOrgs.map((a) => (_jsxs(XStack, { render: "button", "aria-label": a.blocked ? `${a.name} needs an organization admin` : `Open ${a.name}`, disabled: a.blocked, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: () => {
|
|
106
106
|
setOwner(a.name);
|
|
107
107
|
setQ('');
|
|
108
108
|
}, children: [_jsx(SizableText, { flex: 1, size: "$2", color: a.blocked ? '$soft' : '$ink', numberOfLines: 1, children: a.name }), a.blocked ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Needs organization admin" })) : (_jsx(ChevronRight, { size: 14 }))] }, a.name)))), _jsxs(XStack, { render: "button", "aria-label": "Add organization", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), disabled: !signed, children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add organization" })] })] })] })) : null, step === 'pick' && owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { render: "button", "aria-label": "All organizations", onPress: () => setOwner(''), children: _jsx(ChevronLeft, { size: 16 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: owner })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search repositories", "aria-label": "Search repositories" }), _jsxs(XStack, { justify: "space-between", items: "center", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [ofOwner.length, " ", ofOwner.length === 1 ? 'repository' : 'repositories'] }), _jsx(XStack, { render: "button", "aria-label": "Select all", onPress: selectShown, disabled: shownRepos.length === 0, children: _jsx(SizableText, { size: "$1", color: "$ink", children: "Select all" }) })] }), shownRepos.length === 0 ? (_jsx(Soft, { children: "Nothing matches." })) : (shownRepos.map((r) => (_jsx(RepoRow, { repo: r, on: picked.includes(r.fullName), onPress: () => toggle(r.fullName) }, r.fullName)))), _jsxs(XStack, { render: "button", "aria-label": "Grant more repositories", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add repositories" })] })] })] })) : null, step === 'access' ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Repository access" }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: "Review who can read these repositories" })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsxs(XStack, { flex: 1, items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Clear the selection", onPress: () => { setPicked([]); setStep('pick'); }, children: _jsx(X, { size: 12 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: more ? `${lead}, +${more} more` : lead })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setStep('pick'), children: "Edit" })] }), _jsxs(XStack, { justify: "space-between", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Who gets access" }), _jsxs(SizableText, { size: "$2", color: "$ink", children: [host.person?.name || host.person?.email || 'You', " \u00B7 You"] })] }), _jsxs(YStack, { gap: "$1", items: "flex-end", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Role" }), _jsx(SizableText, { size: "$2", color: "$ink", children: host.admin ? 'Repository admin' : 'Member' })] })] }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Anyone with access to ", dest] })] })) : null, error ? _jsx(Soft, { children: error }) : null, step === 'pick' ? (_jsx(Button, { width: "100%", disabled: !owner || picked.length === 0, onPress: () => setStep('access'), children: "Continue" })) : (_jsxs(YStack, { gap: "$3", items: "center", width: "100%", children: [_jsx(Button, { width: "100%", disabled: busy || syncing || picked.length === 0, onPress: () => void send(), children: syncing || busy ? 'Syncing…' : `Sync ${picked.length} ${picked.length === 1 ? 'repository' : 'repositories'} to ${dest}` }), syncing ? null : (_jsx(XStack, { render: "button", "aria-label": "Sync later", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Sync later" }) }))] })), !signed ? (_jsx(Button, { width: "100%", onPress: () => host.signIn?.(), children: "Sign in" })) : null] })] })] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|