@hanzo/build 0.2.8 → 0.2.10
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 +34 -1
- package/lib/api/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +11 -3
- package/lib/api/coding.js +13 -3
- package/lib/api/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +7 -0
- package/lib/api/sessions.js +16 -0
- package/lib/api/turn.js +3 -0
- package/lib/builder.js +5 -1
- package/lib/desk.d.ts +7 -1
- package/lib/desk.js +30 -23
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/forge.js +6 -2
- package/lib/git.d.ts +5 -0
- package/lib/git.js +70 -0
- package/lib/landing.js +18 -1
- package/lib/run.js +37 -7
- package/package.json +4 -2
package/lib/desk.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The pane beside a run's transcript:
|
|
4
|
-
*
|
|
3
|
+
* The pane beside a run's transcript: its codebase's environment, what it
|
|
4
|
+
* pushed, its sandbox's desktop and shell, its files, and what it listens to.
|
|
5
5
|
*/
|
|
6
6
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
7
|
-
import { Copy,
|
|
7
|
+
import { Copy, MoreHorizontal, PanelRightClose } from '@hanzogui/lucide-icons-2';
|
|
8
8
|
import { Button, DropdownMenu } from '@hanzo/ui';
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
-
import { blob, tree } from './api/git.js';
|
|
11
10
|
import { shell } from './api/turn.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
import { Door } from './door.js';
|
|
12
|
+
import { Environment } from './environment.js';
|
|
13
|
+
import { Files } from './files.js';
|
|
14
|
+
import { Git } from './git.js';
|
|
15
|
+
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
16
|
+
const BLEED = ['desktop', 'terminal'];
|
|
17
|
+
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, refused, retry = 'Retry', onRetry, onHide, }) {
|
|
18
|
+
// A setup run's work IS the environment, so it opens there.
|
|
19
|
+
const [tab, setTab] = useState(mode === 'setup' ? 'environment' : 'terminal');
|
|
17
20
|
const [copied, setCopied] = useState(false);
|
|
18
21
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
19
|
-
const ref = branch || base || 'main';
|
|
20
22
|
const lines = shell(events);
|
|
23
|
+
const bleed = BLEED.includes(tab);
|
|
21
24
|
const copy = async () => {
|
|
22
25
|
try {
|
|
23
26
|
await navigator.clipboard.writeText(id);
|
|
@@ -27,10 +30,10 @@ export function Desk({ id, repo, branch, base, environment, mode, pr, events, li
|
|
|
27
30
|
setCopied(false);
|
|
28
31
|
}
|
|
29
32
|
};
|
|
30
|
-
return (_jsxs(YStack, { flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "subscriptions", tab: tab, onPick: setTab, children: "Subscriptions" }), _jsx(XStack, { flex: 1 }), onHide ? (_jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) })) : null, _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
33
|
+
return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "subscriptions", tab: tab, onPick: setTab, children: "Subscriptions" }), _jsx(XStack, { flex: 1 }), onHide ? (_jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) })) : null, _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
31
34
|
{ key: 'copy', label: copied ? 'Copied' : 'Copy run id', icon: _jsx(Copy, { size: 16 }), description: id, onSelect: () => void copy() },
|
|
32
35
|
{ key: 'details', label: 'Details', onSelect: () => setTab('environment') },
|
|
33
|
-
] })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow:
|
|
36
|
+
] })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, live: live, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'files' ? (_jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: live, mode: mode, project: project, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'subscriptions' ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", px: "$6", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 320 }, children: "Ask an agent to subscribe to a Slack channel or thread, a pull request, or a timer." }) })) : null] })] }));
|
|
34
37
|
}
|
|
35
38
|
function TabButton({ id, tab, onPick, children }) {
|
|
36
39
|
const on = tab === id;
|
|
@@ -40,17 +43,21 @@ function Fact({ label, value }) {
|
|
|
40
43
|
return (_jsxs(XStack, { gap: "$3", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 96, children: label }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 2, children: value })] }));
|
|
41
44
|
}
|
|
42
45
|
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace: 'pre' };
|
|
43
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The Terminal tab: a shell in the run's sandbox, in the same working tree as the
|
|
48
|
+
* agent, while the run holds one; and the commands the agent itself ran, which
|
|
49
|
+
* outlive the sandbox. The shell is a tmux session named for the run, so a
|
|
50
|
+
* reopened tab reattaches to it.
|
|
51
|
+
*/
|
|
52
|
+
function Terminal({ id, sandbox, lines, live, refused, retry, onRetry, }) {
|
|
53
|
+
const reachable = live && Boolean(sandbox);
|
|
54
|
+
// The shell while there is one, unless the log was asked for.
|
|
55
|
+
const [log, setLog] = useState(false);
|
|
56
|
+
const showShell = reachable && !log;
|
|
57
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [reachable ? (_jsx(XStack, { render: "button", "aria-label": "Shell", onPress: () => setLog(false), px: "$2", py: "$1", rounded: "$2", bg: showShell ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: showShell ? '$ink' : '$soft', children: "Shell" }) })) : null, _jsx(XStack, { render: "button", "aria-label": "Agent log", onPress: () => setLog(true), px: "$2", py: "$1", rounded: "$2", bg: showShell ? 'transparent' : '$hover', children: _jsx(SizableText, { size: "$1", color: showShell ? '$soft' : '$ink', children: "Agent log" }) })] }), showShell ? _jsx(Door, { which: "terminal", sandbox: sandbox, live: live, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })] }));
|
|
58
|
+
}
|
|
59
|
+
function Log({ lines, live, refused, retry, onRetry, }) {
|
|
44
60
|
const prompt = 'workspace';
|
|
45
61
|
const idle = lines.length === 0 && !live;
|
|
46
|
-
return (_jsxs(YStack, { flex: 1, minH: 0,
|
|
47
|
-
}
|
|
48
|
-
function Files({ repo, refName }) {
|
|
49
|
-
const t = useTarget();
|
|
50
|
-
const [dir, setDir] = useState('');
|
|
51
|
-
const [file, setFile] = useState('');
|
|
52
|
-
const list = useRead(repo ? () => tree(t, repo, refName, dir) : null, [], [t, repo, refName, dir]);
|
|
53
|
-
const body = useRead(file ? () => blob(t, repo, refName, file) : null, null, [t, repo, refName, file]);
|
|
54
|
-
const parent = dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '';
|
|
55
|
-
return (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", children: [dir ? (_jsx(XStack, { render: "button", "aria-label": "Up one directory", onPress: () => { setDir(parent); setFile(''); }, px: "$1", py: "$1", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Up" }) })) : null, _jsxs(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [repo, refName ? `@${refName}` : '', dir ? `/${dir}` : ''] })] }), list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the branch\u2026" })) : !repo ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "This run has no repository yet." })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "This directory is empty." })) : (list.value.map((e) => (_jsxs(XStack, { render: "button", "aria-label": e.dir ? `Open ${e.name}` : `Read ${e.name}`, onPress: () => (e.dir ? (setDir(e.path), setFile('')) : setFile(e.path)), items: "center", gap: "$2", py: "$1", hoverStyle: { bg: '$hover' }, children: [e.dir ? _jsx(Folder, { size: 14 }) : _jsx(File, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: e.name })] }, e.path)))), file && body.value ? (_jsxs(YStack, { gap: "$1", pt: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: file }), _jsx(SizableText, { size: "$1", color: "$ink", style: { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: body.value.binary ? 'Binary file' : body.value.truncated ? 'Too large to show' : body.value.text || 'Empty file' })] })) : null, file && body.error ? (_jsx(SizableText, { size: "$1", color: "$soft", children: body.error.message })) : null] }));
|
|
62
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$3", gap: "$1", children: [idle ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", py: "$8", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: refused || 'This run finished without writing a command.' }), refused ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: retry })) : null] })) : (lines.map((line, i) => line.role === 'cmd' ? (_jsxs(XStack, { gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(SizableText, { flex: 1, size: "$1", color: "$ink", style: mono, children: line.text })] }, i)) : (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: line.text }, i)))), live ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(YStack, { width: 7, height: 14, bg: "$ink" })] })) : null] }), _jsx(XStack, { px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Commands appear here as the run writes them." }) })] }));
|
|
56
63
|
}
|
package/lib/door.d.ts
ADDED
package/lib/door.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One door into a run's sandbox — its screen or its shell — framed from cloud's
|
|
4
|
+
* own page with a fresh ticket in its address.
|
|
5
|
+
*
|
|
6
|
+
* The page says when its socket is up. A page that says it failed, or silence
|
|
7
|
+
* past the deadline, is a failed connection, and Retry mints a new ticket: a
|
|
8
|
+
* ticket is spent the moment its socket opens, so reloading the old address can
|
|
9
|
+
* only fail again.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, YStack } from '@hanzo/gui';
|
|
12
|
+
import { Button } from '@hanzo/ui';
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { door } from './api/sandbox.js';
|
|
15
|
+
import { useTarget } from './host.js';
|
|
16
|
+
/** What each page signs its messages to the window that frames it with. */
|
|
17
|
+
const SOURCE = { screen: 'hanzo-screen', terminal: 'hanzo-term' };
|
|
18
|
+
const WHAT = { screen: 'desktop', terminal: 'terminal' };
|
|
19
|
+
const DEADLINE = 20_000;
|
|
20
|
+
export function Door({ which, sandbox, live, session }) {
|
|
21
|
+
const t = useTarget();
|
|
22
|
+
const frame = useRef(null);
|
|
23
|
+
const [src, setSrc] = useState('');
|
|
24
|
+
const [state, setState] = useState('opening');
|
|
25
|
+
const [why, setWhy] = useState('');
|
|
26
|
+
const [attempt, setAttempt] = useState(0);
|
|
27
|
+
const up = live && Boolean(sandbox);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!up)
|
|
30
|
+
return;
|
|
31
|
+
let gone = false;
|
|
32
|
+
setSrc('');
|
|
33
|
+
setState('opening');
|
|
34
|
+
setWhy('');
|
|
35
|
+
door(t, sandbox, which, session).then((u) => {
|
|
36
|
+
if (!gone)
|
|
37
|
+
setSrc(u);
|
|
38
|
+
}, (e) => {
|
|
39
|
+
if (gone)
|
|
40
|
+
return;
|
|
41
|
+
setState('failed');
|
|
42
|
+
setWhy(e instanceof Error ? e.message : '');
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
gone = true;
|
|
46
|
+
};
|
|
47
|
+
}, [t, sandbox, which, session, up, attempt]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!src)
|
|
50
|
+
return;
|
|
51
|
+
const origin = new URL(src, window.location.href).origin;
|
|
52
|
+
const hear = (e) => {
|
|
53
|
+
if (e.origin !== origin || e.source !== frame.current?.contentWindow)
|
|
54
|
+
return;
|
|
55
|
+
const m = e.data;
|
|
56
|
+
if (!m || m.source !== SOURCE[which])
|
|
57
|
+
return;
|
|
58
|
+
if (m.retry === true) {
|
|
59
|
+
setAttempt((a) => a + 1);
|
|
60
|
+
}
|
|
61
|
+
else if (m.ready === true) {
|
|
62
|
+
setState('open');
|
|
63
|
+
}
|
|
64
|
+
else if (m.ready === false) {
|
|
65
|
+
setState('failed');
|
|
66
|
+
setWhy(typeof m.why === 'string' ? m.why : '');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener('message', hear);
|
|
70
|
+
const late = setTimeout(() => setState((s) => (s === 'opening' ? 'failed' : s)), DEADLINE);
|
|
71
|
+
return () => {
|
|
72
|
+
window.removeEventListener('message', hear);
|
|
73
|
+
clearTimeout(late);
|
|
74
|
+
};
|
|
75
|
+
}, [src, which]);
|
|
76
|
+
if (!live) {
|
|
77
|
+
return _jsx(Shut, { title: "Not running", body: `This run’s ${WHAT[which]} closed when the run stopped.` });
|
|
78
|
+
}
|
|
79
|
+
if (!sandbox) {
|
|
80
|
+
return _jsx(Shut, { title: "Starting", body: `The ${WHAT[which]} opens once the run’s sandbox is up.` });
|
|
81
|
+
}
|
|
82
|
+
if (state === 'failed') {
|
|
83
|
+
return (_jsx(Shut, { title: "Failed to connect", body: why || `Unable to reach the ${WHAT[which]}. The run may have stopped.`, children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: "Retry" }) }));
|
|
84
|
+
}
|
|
85
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", bg: "#000", children: [src ? (_jsx("iframe", { ref: frame, src: src, title: which === 'screen' ? 'The run’s desktop' : 'The run’s terminal', allow: "clipboard-read; clipboard-write", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#000' } })) : null, state === 'opening' ? (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", pointerEvents: "none", children: _jsx(SizableText, { size: "$2", color: "$soft", children: `Connecting to the ${WHAT[which]}…` }) })) : null] }));
|
|
86
|
+
}
|
|
87
|
+
function Shut({ title, body, children }) {
|
|
88
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", px: "$6", py: "$8", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 360 }, children: body }), children] }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function Environment({ repo, busy }: {
|
|
2
|
+
repo: string;
|
|
3
|
+
busy: boolean;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
/**
|
|
6
|
+
* New's offer to set a codebase up. An agent onboards it, or an org admin saves
|
|
7
|
+
* it empty and writes the scripts by hand beside the next run.
|
|
8
|
+
*/
|
|
9
|
+
export declare function SetupDialog({ repo, open, onOpenChange, onStart, onSaved, }: {
|
|
10
|
+
repo: string;
|
|
11
|
+
open: boolean;
|
|
12
|
+
onOpenChange: (o: boolean) => void;
|
|
13
|
+
onStart: () => Promise<void>;
|
|
14
|
+
onSaved: () => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A codebase's environment: the install script, the start command and the
|
|
4
|
+
* secrets a run exports, as /v1/environment holds them.
|
|
5
|
+
*
|
|
6
|
+
* A setup run's proposal is drawn in the editors until it is saved — saving is
|
|
7
|
+
* the review. While a setup run on the codebase is still working the scripts are
|
|
8
|
+
* its to find, so they cannot be edited. A member reads; an org admin saves and
|
|
9
|
+
* sets secrets, which is the platform's rule and not a choice made here.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { Plus, X } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input, Textarea } from '@hanzo/ui';
|
|
14
|
+
import { useEffect, useState } from 'react';
|
|
15
|
+
import { start } from './api/coding.js';
|
|
16
|
+
import { read, refuse, removeSecret, save, setSecret, SETUP } from './api/environment.js';
|
|
17
|
+
import { useRead } from './data.js';
|
|
18
|
+
import { useHost, useTarget } from './host.js';
|
|
19
|
+
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
20
|
+
export function Environment({ repo, busy }) {
|
|
21
|
+
const host = useHost();
|
|
22
|
+
const t = useTarget();
|
|
23
|
+
const signed = Boolean(host.person);
|
|
24
|
+
// Read again when a setup run on it stops, which is when its proposal lands.
|
|
25
|
+
const env = useRead(signed && repo ? () => read(t, repo) : null, null, [t, repo, signed, busy]);
|
|
26
|
+
const [install, setInstall] = useState('');
|
|
27
|
+
const [startCmd, setStart] = useState('');
|
|
28
|
+
const [adding, setAdding] = useState(false);
|
|
29
|
+
const [name, setName] = useState('');
|
|
30
|
+
const [value, setValue] = useState('');
|
|
31
|
+
const [note, setNote] = useState('');
|
|
32
|
+
const [working, setWorking] = useState(false);
|
|
33
|
+
const shown = env.value;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!shown)
|
|
36
|
+
return;
|
|
37
|
+
setInstall(shown.proposal?.install ?? shown.install);
|
|
38
|
+
setStart(shown.proposal?.start ?? shown.start);
|
|
39
|
+
}, [shown]);
|
|
40
|
+
if (!signed) {
|
|
41
|
+
return _jsx(Soft, { children: "Sign in to see this codebase\u2019s environment." });
|
|
42
|
+
}
|
|
43
|
+
if (!repo) {
|
|
44
|
+
return _jsx(Soft, { children: "This run has no codebase, so it has no environment." });
|
|
45
|
+
}
|
|
46
|
+
if (env.error) {
|
|
47
|
+
return _jsx(Soft, { children: env.error.message });
|
|
48
|
+
}
|
|
49
|
+
if (!shown) {
|
|
50
|
+
return _jsx(Soft, { children: "Reading the environment\u2026" });
|
|
51
|
+
}
|
|
52
|
+
const changed = install !== shown.install || startCmd !== shown.start || shown.proposal !== null;
|
|
53
|
+
const needed = (shown.proposal?.secrets ?? []).filter((n) => !shown.secrets.includes(n));
|
|
54
|
+
const status = busy ? 'Setting up' : shown.state === 'proposed' ? 'Proposed' : shown.state === 'ready' ? 'Ready' : 'Not set up';
|
|
55
|
+
/** One write, then the record read again. Answers whether it took. */
|
|
56
|
+
const act = async (what, done = '') => {
|
|
57
|
+
setWorking(true);
|
|
58
|
+
setNote('');
|
|
59
|
+
try {
|
|
60
|
+
await what();
|
|
61
|
+
env.reload();
|
|
62
|
+
setNote(done);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
setWorking(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const setup = async () => {
|
|
74
|
+
setWorking(true);
|
|
75
|
+
setNote('');
|
|
76
|
+
try {
|
|
77
|
+
const run = await start(t, { prompt: SETUP, repo, mode: 'setup' });
|
|
78
|
+
host.go(run.session);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
setNote(e instanceof Error ? e.message : 'The setup run did not start');
|
|
82
|
+
setWorking(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const add = () => {
|
|
86
|
+
const n = name.trim();
|
|
87
|
+
const why = refuse(n) || (value ? '' : 'A secret needs a value');
|
|
88
|
+
if (why) {
|
|
89
|
+
setNote(why);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
void act(() => setSecret(t, repo, n, value), `${n} is set`).then((took) => {
|
|
93
|
+
if (!took)
|
|
94
|
+
return;
|
|
95
|
+
setAdding(false);
|
|
96
|
+
setName('');
|
|
97
|
+
setValue('');
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
return (_jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$4", color: "$ink", numberOfLines: 1, children: repo }), _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(YStack, { width: 6, height: 6, rounded: 999, bg: busy ? '$ink' : shown.state === 'ready' ? '$ink' : '$soft', opacity: busy ? 1 : 0.7 }), _jsx(SizableText, { size: "$1", color: "$soft", children: status })] }), host.admin ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy || working || !changed, onPress: () => void act(() => save(t, repo, { install, start: startCmd }), 'Saved'), children: "Save" })) : null] }), shown.proposal?.note ? (_jsxs(YStack, { gap: "$1", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "What the setup agent found" }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 8, children: shown.proposal.note })] })) : null, _jsx(Script, { label: "Install Script", value: install, onChange: setInstall, locked: busy || !host.admin, hint: busy ? 'Install script editing will be available when the agent stops running' : 'pnpm install --frozen-lockfile' }), _jsx(Script, { label: "Start Script", value: startCmd, onChange: setStart, locked: busy || !host.admin, hint: busy ? 'Start script editing will be available when the agent stops running' : 'pnpm dev' }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, children: "Secrets" }), host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), "New Secret"] })) : null] }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [shown.secrets.length === 0 && needed.length === 0 && !adding ? (_jsx(YStack, { py: "$5", items: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", children: "No secrets yet" }) })) : null, shown.secrets.map((s, i) => (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", style: mono, children: s }), _jsx(SizableText, { size: "$1", color: "$soft", children: "set" }), host.admin ? (_jsx(XStack, { render: "button", "aria-label": `Remove ${s}`, px: "$1", onPress: () => void act(() => removeSecret(t, repo, s), `${s} is removed`), children: _jsx(X, { size: 14 }) })) : null] }, s))), needed.map((s) => (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", style: mono, children: s }), _jsx(SizableText, { size: "$1", color: "$soft", children: "needed, not set" }), host.admin ? (_jsx(XStack, { render: "button", "aria-label": `Set ${s}`, px: "$1", onPress: () => { setAdding(true); setName(s); }, children: _jsx(Plus, { size: 14 }) })) : null] }, s))), adding ? (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: shown.secrets.length || needed.length ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Input, { value: name, onChangeText: setName, placeholder: "NAME", "aria-label": "Secret name", autoCapitalize: "characters" }), _jsx(Input, { value: value, onChangeText: setValue, placeholder: "Value", "aria-label": "Secret value", secureTextEntry: true }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => { setAdding(false); setName(''); setValue(''); }, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: add, children: "Add secret" })] })] })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Values are sealed in KMS and exported to every run on this codebase. They are never shown again." })] }), shown.state === 'none' && !busy ? (_jsxs(YStack, { gap: "$2", items: "flex-start", children: [_jsx(Button, { size: "sm", disabled: working, onPress: () => void setup(), children: "Set up with an agent" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "An agent explores the codebase, installs and checks it, and proposes the scripts for you to review and save." })] })) : null, !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin saves the environment and sets its secrets." })) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* New's offer to set a codebase up. An agent onboards it, or an org admin saves
|
|
104
|
+
* it empty and writes the scripts by hand beside the next run.
|
|
105
|
+
*/
|
|
106
|
+
export function SetupDialog({ repo, open, onOpenChange, onStart, onSaved, }) {
|
|
107
|
+
const host = useHost();
|
|
108
|
+
const t = useTarget();
|
|
109
|
+
const [working, setWorking] = useState(false);
|
|
110
|
+
const [note, setNote] = useState('');
|
|
111
|
+
const run = async (what, done) => {
|
|
112
|
+
setWorking(true);
|
|
113
|
+
setNote('');
|
|
114
|
+
try {
|
|
115
|
+
await what();
|
|
116
|
+
done();
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
setWorking(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Set up an environment" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Repository" }), _jsx(XStack, { px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: repo }) })] }), _jsx(SizableText, { size: "$2", color: "$soft", children: "An agent onboards the codebase: it explores it, writes the install and start scripts, and names the secrets it needs. It takes several minutes. Interrupt it anytime, or take over in the terminal." }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "space-between", items: "center", children: [host.admin ? (_jsx(Button, { size: "sm", variant: "secondary", disabled: working, onPress: () => void run(() => save(t, repo, { install: '', start: '' }), onSaved), children: "Skip & save" })) : (_jsx(YStack, {})), _jsx(Button, { size: "sm", variant: "primary", disabled: working, onPress: () => void run(onStart, () => onOpenChange(false)), children: "Start agent" })] })] })] }) }));
|
|
126
|
+
}
|
|
127
|
+
function Script({ label, value, onChange, locked, hint }) {
|
|
128
|
+
return (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: label }), _jsx(Textarea, { value: value, onChangeText: onChange, disabled: locked, placeholder: hint, "aria-label": label, rows: 3, style: mono })] }));
|
|
129
|
+
}
|
|
130
|
+
function Soft({ children }) {
|
|
131
|
+
return (_jsx(YStack, { py: "$6", items: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }) }));
|
|
132
|
+
}
|
package/lib/files.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function Files({ session, repo, branch, sandbox, live, mode, project, pr, onEnvironment, }: {
|
|
2
|
+
session: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
sandbox: string;
|
|
6
|
+
live: boolean;
|
|
7
|
+
mode: string;
|
|
8
|
+
project: string;
|
|
9
|
+
pr: {
|
|
10
|
+
href: string;
|
|
11
|
+
label: string;
|
|
12
|
+
};
|
|
13
|
+
onEnvironment: () => void;
|
|
14
|
+
}): import("react").JSX.Element;
|
package/lib/files.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A run's Files tab. Files is its working tree: live from the sandbox while the
|
|
4
|
+
* run holds one — every edit the agent has made, committed or not — and its
|
|
5
|
+
* branch on the forge otherwise. Artifacts is what the run produced: its pull
|
|
6
|
+
* request, its branch, its environment proposal and the project it built.
|
|
7
|
+
*/
|
|
8
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { ExternalLink, File, Folder, GitBranch, GitPullRequest, Layers, Settings2 } from '@hanzogui/lucide-icons-2';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { blob, tree } from './api/changes.js';
|
|
12
|
+
import { read as look } from './api/sandbox.js';
|
|
13
|
+
import { useRead } from './data.js';
|
|
14
|
+
import { useHost, useTarget } from './host.js';
|
|
15
|
+
import { Out } from './out.js';
|
|
16
|
+
const monoWrap = { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
17
|
+
export function Files({ session, repo, branch, sandbox, live, mode, project, pr, onEnvironment, }) {
|
|
18
|
+
const [pane, setPane] = useState('files');
|
|
19
|
+
const reachable = live && Boolean(sandbox);
|
|
20
|
+
const [source, setSource] = useState(reachable ? 'live' : 'branch');
|
|
21
|
+
const from = reachable ? source : 'branch';
|
|
22
|
+
return (_jsxs(YStack, { gap: "$3", children: [_jsxs(XStack, { gap: "$1", items: "center", children: [_jsx(Pick, { on: pane === 'files', onPress: () => setPane('files'), children: "Files" }), _jsx(Pick, { on: pane === 'artifacts', onPress: () => setPane('artifacts'), children: "Artifacts" }), _jsx(XStack, { flex: 1 }), pane === 'files' && reachable ? (_jsxs(_Fragment, { children: [_jsx(Pick, { on: from === 'live', onPress: () => setSource('live'), children: "Live" }), _jsx(Pick, { on: from === 'branch', onPress: () => setSource('branch'), children: "Branch" })] })) : null] }), pane === 'artifacts' ? (_jsx(Artifacts, { repo: repo, branch: branch, mode: mode, project: project, pr: pr, onEnvironment: onEnvironment })) : from === 'live' ? (_jsx(Live, { sandbox: sandbox })) : (_jsx(Branch, { session: session, label: `${repo}${branch ? `@${branch}` : ''}` }))] }));
|
|
23
|
+
}
|
|
24
|
+
/** The sandbox's working tree, as it is right now. */
|
|
25
|
+
function Live({ sandbox }) {
|
|
26
|
+
const t = useTarget();
|
|
27
|
+
const [dir, setDir] = useState('');
|
|
28
|
+
const [file, setFile] = useState('');
|
|
29
|
+
const list = useRead(() => look(t, sandbox, dir), null, [t, sandbox, dir]);
|
|
30
|
+
const body = useRead(file ? () => look(t, sandbox, file) : null, null, [t, sandbox, file]);
|
|
31
|
+
const at = (name) => (dir ? `${dir}/${name}` : name);
|
|
32
|
+
// A name is only a name until it is read: the answer says whether it was a directory.
|
|
33
|
+
const open = async (name) => {
|
|
34
|
+
const path = at(name);
|
|
35
|
+
try {
|
|
36
|
+
const n = await look(t, sandbox, path);
|
|
37
|
+
if (n.dir) {
|
|
38
|
+
setDir(path);
|
|
39
|
+
setFile('');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
setFile(path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
setFile(path);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return (_jsx(Listing, { where: `/work${dir ? `/${dir}` : ''}`, up: dir ? () => (setDir(dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : ''), setFile('')) : undefined, error: list.error?.message ?? '', loading: list.loading && !list.value, rows: (list.value?.names ?? []).map((name) => ({ key: name, name, dir: null, onPress: () => void open(name) })), file: file, body: body.error
|
|
50
|
+
? body.error.message
|
|
51
|
+
: body.value
|
|
52
|
+
? body.value.binary
|
|
53
|
+
? 'Binary file'
|
|
54
|
+
: body.value.truncated
|
|
55
|
+
? 'Too large to show'
|
|
56
|
+
: body.value.text || 'Empty file'
|
|
57
|
+
: '' }));
|
|
58
|
+
}
|
|
59
|
+
/** The run's branch on the forge; its base until it has pushed. */
|
|
60
|
+
function Branch({ session, label }) {
|
|
61
|
+
const host = useHost();
|
|
62
|
+
const t = useTarget();
|
|
63
|
+
const signed = Boolean(host.person);
|
|
64
|
+
const [dir, setDir] = useState('');
|
|
65
|
+
const [file, setFile] = useState('');
|
|
66
|
+
const list = useRead(signed ? () => tree(t, session, dir) : null, [], [t, session, dir, signed]);
|
|
67
|
+
const body = useRead(file ? () => blob(t, session, file) : null, null, [t, session, file]);
|
|
68
|
+
if (!signed)
|
|
69
|
+
return _jsx(Soft, { children: "Sign in to read this run\u2019s files." });
|
|
70
|
+
return (_jsx(Listing, { where: `${label}${dir ? `/${dir}` : ''}`, up: dir ? () => (setDir(dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : ''), setFile('')) : undefined, error: list.error?.message ?? '', loading: list.loading && list.value.length === 0, rows: list.value.map((e) => ({
|
|
71
|
+
key: e.path,
|
|
72
|
+
name: e.name,
|
|
73
|
+
dir: e.dir,
|
|
74
|
+
onPress: () => (e.dir ? (setDir(e.path), setFile('')) : setFile(e.path)),
|
|
75
|
+
})), file: file, body: body.error
|
|
76
|
+
? body.error.message
|
|
77
|
+
: body.value
|
|
78
|
+
? body.value.binary
|
|
79
|
+
? 'Binary file'
|
|
80
|
+
: body.value.truncated
|
|
81
|
+
? 'Too large to show'
|
|
82
|
+
: body.value.text || 'Empty file'
|
|
83
|
+
: '' }));
|
|
84
|
+
}
|
|
85
|
+
function Listing({ where, up, error, loading, rows, file, body, }) {
|
|
86
|
+
return (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", children: [up ? (_jsx(XStack, { render: "button", "aria-label": "Up one directory", onPress: up, px: "$1", py: "$1", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Up" }) })) : null, _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: where })] }), error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: error })) : loading ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading\u2026" })) : rows.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "This directory is empty." })) : (rows.map((r) => (_jsxs(XStack, { render: "button", "aria-label": r.dir === false ? `Read ${r.name}` : `Open ${r.name}`, onPress: r.onPress, items: "center", gap: "$2", py: "$1", hoverStyle: { bg: '$hover' }, children: [r.dir === false ? _jsx(File, { size: 14 }) : r.dir ? _jsx(Folder, { size: 14 }) : _jsx(File, { size: 14, opacity: 0.6 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: r.name })] }, r.key)))), file && body ? (_jsxs(YStack, { gap: "$1", pt: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: file }), _jsx(SizableText, { size: "$1", color: "$ink", style: monoWrap, children: body })] })) : null] }));
|
|
87
|
+
}
|
|
88
|
+
function Artifacts({ repo, branch, mode, project, pr, onEnvironment, }) {
|
|
89
|
+
const host = useHost();
|
|
90
|
+
const items = [];
|
|
91
|
+
if (pr.href)
|
|
92
|
+
items.push({ key: 'pr', icon: _jsx(GitPullRequest, { size: 16 }), title: `Pull request ${pr.label}`, note: 'Opened on the forge', href: pr.href });
|
|
93
|
+
if (branch)
|
|
94
|
+
items.push({ key: 'branch', icon: _jsx(GitBranch, { size: 16 }), title: branch, note: repo ? `The branch this run pushes to in ${repo}` : 'The branch this run pushes to' });
|
|
95
|
+
if (mode === 'setup')
|
|
96
|
+
items.push({ key: 'environment', icon: _jsx(Settings2, { size: 16 }), title: 'Environment proposal', note: 'The install and start scripts this run found', onPress: onEnvironment });
|
|
97
|
+
if (project)
|
|
98
|
+
items.push({ key: 'project', icon: _jsx(Layers, { size: 16 }), title: project, note: 'The project this run builds', onPress: () => host.go(project) });
|
|
99
|
+
if (!items.length)
|
|
100
|
+
return _jsx(Soft, { children: "This run has produced no artifacts yet." });
|
|
101
|
+
return (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: items.map((a, i) => {
|
|
102
|
+
const row = (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: a.href || a.onPress ? { bg: '$hover' } : undefined, children: [a.icon, _jsxs(YStack, { flex: 1, minW: 0, style: { textAlign: 'left' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: a.title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: a.note })] }), a.href ? _jsx(ExternalLink, { size: 14 }) : null] }));
|
|
103
|
+
return a.href ? (_jsx(Out, { href: a.href, label: a.title, children: _jsx(YStack, { width: "100%", children: row }) }, a.key)) : a.onPress ? (_jsx(YStack, { render: "button", "aria-label": a.title, onPress: a.onPress, children: row }, a.key)) : (_jsx(YStack, { children: row }, a.key));
|
|
104
|
+
}) }));
|
|
105
|
+
}
|
|
106
|
+
function Pick({ on, onPress, children }) {
|
|
107
|
+
return (_jsx(XStack, { render: "button", "aria-label": children, onPress: onPress, px: "$2.5", py: "$1", rounded: "$2", bg: on ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', children: children }) }));
|
|
108
|
+
}
|
|
109
|
+
function Soft({ children }) {
|
|
110
|
+
return (_jsx(YStack, { py: "$6", items: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }) }));
|
|
111
|
+
}
|
package/lib/forge.js
CHANGED
|
@@ -13,6 +13,7 @@ import { useEffect, useState } from 'react';
|
|
|
13
13
|
import { revealAccount } from './account.js';
|
|
14
14
|
import { add, arm, flows } from './api/auto.js';
|
|
15
15
|
import { codebases, create } from './api/codebases.js';
|
|
16
|
+
import { environments } from './api/environment.js';
|
|
16
17
|
import { boards, issues } from './api/work.js';
|
|
17
18
|
import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './choice.js';
|
|
18
19
|
import { useKept, useRead } from './data.js';
|
|
@@ -71,7 +72,7 @@ function Automations() {
|
|
|
71
72
|
setProblem(e instanceof Error ? e.message : 'Could not change that automation');
|
|
72
73
|
}
|
|
73
74
|
};
|
|
74
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: "Automations" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Repeating work for this organization. A new one starts off, so you can name it before it runs." })] }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New automation"] })] }), _jsx(XStack, { justify: "flex-end", children: _jsx(YStack, { width: 240, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search\u2026", "aria-label": "Search automations", disabled: !signed }) }) }), problem ? _jsx(Note, { children: problem }) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Empty, { says: "Sign in to see this organization's automations." })) : list.loading && list.value.length === 0 ? (_jsx(Empty, { says: "Reading automations\u2026" })) : shown.length === 0 ? (_jsx(Empty, { says: list.value.length === 0 ? 'No automations yet.' : 'Nothing matches.', children: list.value.length === 0 ? (_jsx(Button, { size: "sm", onPress: () => setMaking(true), children: "New automation" })) : null })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Name" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 80, style: { textAlign: 'right' }, children: "Status" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: "Updated" })] }), shown.map((a) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: a.name }), _jsx(XStack, { render: "button", "aria-label": a.status === 'ENABLED' ? `Turn off ${a.name}` : `Turn on ${a.name}`, onPress: () => void flip(a), width: 80, justify: "flex-end", children: _jsx(SizableText, { size: "$1", color: "$ink", children: a.status === 'ENABLED' ? 'On' : 'Off' }) }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: a.updated ? when(new Date(a.updated).toISOString()) : '—' })] }, a.id)))] }))] }), _jsx(NewAutomation, { open: making, onOpenChange: setMaking, onCreate: async (name) => {
|
|
75
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: "Automations" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Repeating work for this organization. A new one starts off, so you can name it before it runs." })] }), _jsxs(Button, { size: "sm", shrink: 0, disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New automation"] })] }), _jsx(XStack, { justify: "flex-end", children: _jsx(YStack, { width: 240, maxW: "100%", children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search\u2026", "aria-label": "Search automations", disabled: !signed }) }) }), problem ? _jsx(Note, { children: problem }) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Empty, { says: "Sign in to see this organization's automations." })) : list.loading && list.value.length === 0 ? (_jsx(Empty, { says: "Reading automations\u2026" })) : shown.length === 0 ? (_jsx(Empty, { says: list.value.length === 0 ? 'No automations yet.' : 'Nothing matches.', children: list.value.length === 0 ? (_jsx(Button, { size: "sm", onPress: () => setMaking(true), children: "New automation" })) : null })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Name" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 80, style: { textAlign: 'right' }, children: "Status" }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: "Updated" })] }), shown.map((a) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: a.name }), _jsx(XStack, { render: "button", "aria-label": a.status === 'ENABLED' ? `Turn off ${a.name}` : `Turn on ${a.name}`, onPress: () => void flip(a), width: 80, justify: "flex-end", children: _jsx(SizableText, { size: "$1", color: "$ink", children: a.status === 'ENABLED' ? 'On' : 'Off' }) }), _jsx(SizableText, { size: "$1", color: "$soft", width: 120, style: { textAlign: 'right' }, children: a.updated ? when(new Date(a.updated).toISOString()) : '—' })] }, a.id)))] }))] }), _jsx(NewAutomation, { open: making, onOpenChange: setMaking, onCreate: async (name) => {
|
|
75
76
|
await add(t, name);
|
|
76
77
|
setMaking(false);
|
|
77
78
|
setTick((n) => n + 1);
|
|
@@ -114,6 +115,9 @@ function Codebases() {
|
|
|
114
115
|
setPage(0);
|
|
115
116
|
}, [q, sort]);
|
|
116
117
|
const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, making, landed]);
|
|
118
|
+
// Which codebases have an environment. A codebase absent here has none.
|
|
119
|
+
const envs = useRead(signed ? () => environments(t) : null, [], [t, signed]);
|
|
120
|
+
const envOf = new Map(envs.value.map((e) => [e.repo, e.state]));
|
|
117
121
|
const needle = q.trim().toLowerCase();
|
|
118
122
|
const pending = readPending(host.org);
|
|
119
123
|
const pendingKey = pending.map((p) => p.fullName).join('\n');
|
|
@@ -173,7 +177,7 @@ function Codebases() {
|
|
|
173
177
|
pinCodebase(host.org, c);
|
|
174
178
|
host.go('');
|
|
175
179
|
};
|
|
176
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => revealAccount(), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
|
|
180
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => revealAccount(), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
|
|
177
181
|
const row = await create(t, name, description);
|
|
178
182
|
pinCodebase(host.org, row);
|
|
179
183
|
setMaking(false);
|