@hanzo/build 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +126 -23
  2. package/lib/api/agents.d.ts +80 -0
  3. package/lib/api/agents.js +148 -0
  4. package/lib/api/billing.d.ts +205 -0
  5. package/lib/api/billing.js +300 -0
  6. package/lib/api/call.d.ts +6 -0
  7. package/lib/api/call.js +11 -0
  8. package/lib/api/capabilities.d.ts +25 -0
  9. package/lib/api/capabilities.js +40 -0
  10. package/lib/api/changes.d.ts +56 -0
  11. package/lib/api/changes.js +63 -0
  12. package/lib/api/coding.d.ts +42 -4
  13. package/lib/api/coding.js +62 -3
  14. package/lib/api/connectors.d.ts +90 -0
  15. package/lib/api/connectors.js +109 -0
  16. package/lib/api/consent.d.ts +22 -0
  17. package/lib/api/consent.js +28 -0
  18. package/lib/api/environment.d.ts +57 -0
  19. package/lib/api/environment.js +85 -0
  20. package/lib/api/git.d.ts +6 -2
  21. package/lib/api/git.js +13 -5
  22. package/lib/api/github.d.ts +13 -0
  23. package/lib/api/github.js +9 -0
  24. package/lib/api/harness.d.ts +44 -0
  25. package/lib/api/harness.js +348 -0
  26. package/lib/api/keys.d.ts +41 -0
  27. package/lib/api/keys.js +55 -0
  28. package/lib/api/machines.d.ts +56 -0
  29. package/lib/api/machines.js +84 -0
  30. package/lib/api/members.d.ts +47 -0
  31. package/lib/api/members.js +88 -0
  32. package/lib/api/memory.d.ts +25 -0
  33. package/lib/api/memory.js +34 -0
  34. package/lib/api/places.d.ts +2 -2
  35. package/lib/api/places.js +3 -26
  36. package/lib/api/plugins.d.ts +55 -0
  37. package/lib/api/plugins.js +62 -0
  38. package/lib/api/pref.d.ts +49 -0
  39. package/lib/api/pref.js +68 -0
  40. package/lib/api/profile.d.ts +21 -0
  41. package/lib/api/profile.js +40 -0
  42. package/lib/api/projects.d.ts +19 -2
  43. package/lib/api/projects.js +29 -4
  44. package/lib/api/provider.d.ts +42 -0
  45. package/lib/api/provider.js +58 -0
  46. package/lib/api/sandbox.d.ts +30 -0
  47. package/lib/api/sandbox.js +49 -0
  48. package/lib/api/sessions.d.ts +39 -4
  49. package/lib/api/sessions.js +58 -6
  50. package/lib/api/skills.d.ts +70 -0
  51. package/lib/api/skills.js +90 -0
  52. package/lib/api/tools.d.ts +34 -0
  53. package/lib/api/tools.js +37 -0
  54. package/lib/api/turn.d.ts +84 -16
  55. package/lib/api/turn.js +224 -4
  56. package/lib/api/webhooks.d.ts +60 -0
  57. package/lib/api/webhooks.js +81 -0
  58. package/lib/ask.d.ts +18 -0
  59. package/lib/ask.js +69 -0
  60. package/lib/builder.js +39 -19
  61. package/lib/customize/agents.d.ts +2 -0
  62. package/lib/customize/agents.js +125 -0
  63. package/lib/customize/connectors.d.ts +2 -0
  64. package/lib/customize/connectors.js +197 -0
  65. package/lib/customize/index.d.ts +6 -0
  66. package/lib/customize/index.js +50 -0
  67. package/lib/customize/plugins.d.ts +2 -0
  68. package/lib/customize/plugins.js +93 -0
  69. package/lib/customize/skills.d.ts +2 -0
  70. package/lib/customize/skills.js +125 -0
  71. package/lib/customize/ui.d.ts +110 -0
  72. package/lib/customize/ui.js +99 -0
  73. package/lib/desk.d.ts +8 -1
  74. package/lib/desk.js +29 -35
  75. package/lib/door.d.ts +7 -0
  76. package/lib/door.js +89 -0
  77. package/lib/environment.d.ts +15 -0
  78. package/lib/environment.js +132 -0
  79. package/lib/files.d.ts +14 -0
  80. package/lib/files.js +111 -0
  81. package/lib/find.d.ts +7 -0
  82. package/lib/find.js +77 -0
  83. package/lib/foot.d.ts +16 -0
  84. package/lib/foot.js +36 -0
  85. package/lib/forge.js +8 -4
  86. package/lib/git.d.ts +9 -0
  87. package/lib/git.js +74 -0
  88. package/lib/host.d.ts +5 -0
  89. package/lib/landing.d.ts +10 -0
  90. package/lib/landing.js +47 -14
  91. package/lib/markdown.d.ts +49 -0
  92. package/lib/markdown.js +123 -0
  93. package/lib/plans.d.ts +1 -0
  94. package/lib/plans.js +113 -0
  95. package/lib/prefs.d.ts +30 -0
  96. package/lib/prefs.js +83 -0
  97. package/lib/prose.d.ts +4 -0
  98. package/lib/prose.js +53 -0
  99. package/lib/route.d.ts +16 -1
  100. package/lib/route.js +32 -1
  101. package/lib/run.js +207 -28
  102. package/lib/section.js +2 -2
  103. package/lib/settings/account.d.ts +5 -0
  104. package/lib/settings/account.js +111 -0
  105. package/lib/settings/billing.d.ts +1 -0
  106. package/lib/settings/billing.js +104 -0
  107. package/lib/settings/capabilities.d.ts +1 -0
  108. package/lib/settings/capabilities.js +55 -0
  109. package/lib/settings/card.d.ts +31 -0
  110. package/lib/settings/card.js +105 -0
  111. package/lib/settings/code.d.ts +1 -0
  112. package/lib/settings/code.js +51 -0
  113. package/lib/settings/environments.d.ts +1 -0
  114. package/lib/settings/environments.js +53 -0
  115. package/lib/settings/general.d.ts +1 -0
  116. package/lib/settings/general.js +44 -0
  117. package/lib/settings/index.d.ts +4 -0
  118. package/lib/settings/index.js +22 -0
  119. package/lib/settings/integrations.d.ts +1 -0
  120. package/lib/settings/integrations.js +81 -0
  121. package/lib/settings/keys.d.ts +1 -0
  122. package/lib/settings/keys.js +102 -0
  123. package/lib/settings/machines.d.ts +1 -0
  124. package/lib/settings/machines.js +118 -0
  125. package/lib/settings/members.d.ts +1 -0
  126. package/lib/settings/members.js +67 -0
  127. package/lib/settings/memory.d.ts +1 -0
  128. package/lib/settings/memory.js +56 -0
  129. package/lib/settings/notifications.d.ts +1 -0
  130. package/lib/settings/notifications.js +97 -0
  131. package/lib/settings/privacy.d.ts +1 -0
  132. package/lib/settings/privacy.js +63 -0
  133. package/lib/settings/sections.d.ts +16 -0
  134. package/lib/settings/sections.js +32 -0
  135. package/lib/settings/ui.d.ts +49 -0
  136. package/lib/settings/ui.js +47 -0
  137. package/lib/settings/usage.d.ts +1 -0
  138. package/lib/settings/usage.js +114 -0
  139. package/lib/shelf.d.ts +6 -0
  140. package/lib/shelf.js +86 -16
  141. package/lib/switch.js +1 -2
  142. package/lib/transcript.d.ts +12 -0
  143. package/lib/transcript.js +56 -0
  144. package/lib/voice.d.ts +3 -1
  145. package/lib/voice.js +10 -4
  146. package/package.json +1 -1
  147. package/lib/account.d.ts +0 -14
  148. package/lib/account.js +0 -38
  149. package/lib/e2e/site.spec.d.ts +0 -1
  150. package/lib/e2e/site.spec.js +0 -125
  151. package/lib/mcp.d.ts +0 -1
  152. package/lib/mcp.js +0 -28
@@ -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/find.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { RailSession } from '@hanzo/ui/chat';
2
+ export declare function Find({ open, onOpenChange, recents, onOpen, }: {
3
+ open: boolean;
4
+ onOpenChange: (o: boolean) => void;
5
+ recents: RailSession[];
6
+ onOpen: (id: string) => void;
7
+ }): import("react").JSX.Element;
package/lib/find.js ADDED
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Finding a run: the rail's search icon. The org's coding runs as the platform
4
+ * lists them, newest first, filtered by status and paged back as far as they
5
+ * go; what is typed narrows the runs read so far by name and codebase. Until
6
+ * the first page lands it searches the rail's own recents.
7
+ */
8
+ import { ScrollView, SizableText, XStack, YStack } from '@hanzo/gui';
9
+ import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
10
+ import { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { page } from './api/sessions.js';
12
+ import { useHost, useTarget } from './host.js';
13
+ /** The statuses the platform filters on, and every run. */
14
+ const FILTERS = [
15
+ { id: '', label: 'All' },
16
+ { id: 'running', label: 'Running' },
17
+ { id: 'paused', label: 'Paused' },
18
+ { id: 'done', label: 'Done' },
19
+ { id: 'error', label: 'Error' },
20
+ ];
21
+ const PAGE = 50;
22
+ export function Find({ open, onOpenChange, recents, onOpen, }) {
23
+ const host = useHost();
24
+ const t = useTarget();
25
+ const signed = Boolean(host.person);
26
+ const [q, setQ] = useState('');
27
+ const [status, setStatus] = useState('');
28
+ const [rows, setRows] = useState(null);
29
+ const [next, setNext] = useState('');
30
+ const [loading, setLoading] = useState(false);
31
+ const [error, setError] = useState('');
32
+ // A page asked for under another status or an earlier opening lands on nothing.
33
+ const asked = useRef(0);
34
+ const read = (after) => {
35
+ const mine = after ? asked.current : ++asked.current;
36
+ setLoading(true);
37
+ setError('');
38
+ return page(t, { kind: 'coding', status, limit: PAGE, after })
39
+ .then((p) => {
40
+ if (mine !== asked.current)
41
+ return;
42
+ setRows((prev) => (after && prev ? [...prev, ...p.sessions.filter((s) => !prev.some((r) => r.id === s.id))] : p.sessions));
43
+ setNext(p.next);
44
+ })
45
+ .catch((e) => {
46
+ if (mine === asked.current)
47
+ setError(e instanceof Error ? e.message : 'The runs could not be read');
48
+ })
49
+ .finally(() => {
50
+ if (mine === asked.current)
51
+ setLoading(false);
52
+ });
53
+ };
54
+ // Opening, or another status, reads from the newest again.
55
+ useEffect(() => {
56
+ if (!open || !signed)
57
+ return;
58
+ setRows(null);
59
+ setNext('');
60
+ void read('');
61
+ // `read` closes over the status and target this effect is keyed on.
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [open, signed, status, t]);
64
+ const hits = useMemo(() => {
65
+ const n = q.trim().toLowerCase();
66
+ const all = rows
67
+ ? rows.map((s) => ({ id: s.id, title: s.title || 'Untitled run', line: [s.repo, s.status].filter(Boolean).join(' · ') }))
68
+ : status
69
+ ? []
70
+ : recents.map((r) => ({ id: r.id, title: r.title, line: '' }));
71
+ return n ? all.filter((h) => `${h.title} ${h.line}`.toLowerCase().includes(n)) : all;
72
+ }, [q, rows, recents, status]);
73
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, children: [_jsx(DialogTitle, { children: "Find a run" }), _jsx(Input, { autoFocus: true, value: q, onChangeText: setQ, placeholder: "Search runs\u2026", "aria-label": "Search runs", onKeyDown: (e) => {
74
+ if ((e.key ?? e.nativeEvent?.key) === 'Enter' && hits[0])
75
+ onOpen(hits[0].id);
76
+ } }), _jsx(XStack, { gap: "$1", flexWrap: "wrap", role: "group", "aria-label": "Status", children: FILTERS.map((f) => (_jsx(XStack, { render: "button", "aria-label": f.label, "aria-pressed": status === f.id, onPress: () => setStatus(f.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: status === f.id ? '$ink' : '$borderColor', bg: status === f.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$1", color: status === f.id ? '$ink' : '$soft', children: f.label }) }, f.id || 'all'))) }), _jsx(ScrollView, { maxH: 360, children: _jsxs(YStack, { gap: "$1", role: "list", children: [hits.map((h) => (_jsxs(XStack, { role: "listitem", render: "button", "aria-label": h.title, onPress: () => onOpen(h.id), px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, items: "center", gap: "$3", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: h.title }), h.line ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, shrink: 0, maxW: "45%", children: h.line })) : null] }, h.id))), !loading && !error && hits.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", children: rows && next ? 'No run read so far matches.' : 'No run matches.' })) : null] }) }), error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: error })) : null, loading ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: "Reading runs\u2026" })) : next ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => void read(next), children: "Older runs" }) })) : null] }) }));
77
+ }
package/lib/foot.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export declare const DOCS = "https://docs.hanzo.ai/docs/dev";
2
+ /** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
3
+ export declare function Slack({ onOpen, onDismiss }: {
4
+ onOpen: () => void;
5
+ onDismiss: () => void;
6
+ }): import("react").JSX.Element;
7
+ /**
8
+ * The account menu. The rail draws the account row and says when it was pressed,
9
+ * so the menu hangs off an anchor laid over the rail's foot: above it on the
10
+ * open rail, beside it on the collapsed one.
11
+ */
12
+ export declare function Who({ open, onOpenChange, narrow }: {
13
+ open: boolean;
14
+ onOpenChange: (o: boolean) => void;
15
+ narrow: boolean;
16
+ }): import("react").JSX.Element;
package/lib/foot.js ADDED
@@ -0,0 +1,36 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The rail's foot: the card that offers Hanzo in Slack, and the menu the account
4
+ * row opens — who is signed in, Settings, Usage, the plans, help and logging out.
5
+ * Everything it opens is an address of this page, but help, which is the
6
+ * documentation.
7
+ */
8
+ import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
9
+ import { Gauge, LifeBuoy, LogOut, Settings, Slack as Mark, Sparkles, X } from '@hanzogui/lucide-icons-2';
10
+ import { MenuRow, MenuRule } from '@hanzo/ui/product';
11
+ import { useHost } from './host.js';
12
+ import { path } from './route.js';
13
+ export const DOCS = 'https://docs.hanzo.ai/docs/dev';
14
+ /** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
15
+ export function Slack({ onOpen, onDismiss }) {
16
+ return (_jsxs(XStack, { items: "center", gap: "$2.5", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(Mark, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: "Try Hanzo in Slack" }), _jsx(XStack, { render: "button", "aria-label": "Set up Hanzo in Slack", onPress: onOpen, cursor: "pointer", children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Set up" }) })] }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: onDismiss, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 14 }) })] }));
17
+ }
18
+ /**
19
+ * The account menu. The rail draws the account row and says when it was pressed,
20
+ * so the menu hangs off an anchor laid over the rail's foot: above it on the
21
+ * open rail, beside it on the collapsed one.
22
+ */
23
+ export function Who({ open, onOpenChange, narrow }) {
24
+ const host = useHost();
25
+ const go = (p) => {
26
+ onOpenChange(false);
27
+ host.go(p);
28
+ };
29
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 6, children: [_jsx(Popover.Anchor, { position: "absolute", l: 8, b: 8, width: narrow ? 48 : 256, height: 36, pointerEvents: "none" }), _jsx(Popover.Content, { role: "menu", "aria-label": "Account", items: "stretch", borderWidth: 1, elevation: "$2", px: "$1", py: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person?.email || host.person?.name || '' }), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", icon: _jsx(Settings, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(MenuRow, { label: "Usage", icon: _jsx(Gauge, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(MenuRow, { label: "View all plans", icon: _jsx(Sparkles, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(MenuRow, { label: "Get help", icon: _jsx(LifeBuoy, { size: 16 }), onPress: () => {
30
+ onOpenChange(false);
31
+ window.open(DOCS, '_blank', 'noopener,noreferrer');
32
+ } }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuRow, { label: "Log out", icon: _jsx(LogOut, { size: 16 }), onPress: () => {
33
+ onOpenChange(false);
34
+ host.signOut?.();
35
+ } })] })) : null] }) })] }));
36
+ }
package/lib/forge.js CHANGED
@@ -10,15 +10,15 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
10
  import { ChevronDown, GitBranch, Plus, Settings, Workflow } from '@hanzogui/lucide-icons-2';
11
11
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
12
12
  import { useEffect, useState } from 'react';
13
- import { revealAccount } from './account.js';
14
13
  import { add, arm, flows } from './api/auto.js';
15
14
  import { codebases, create } from './api/codebases.js';
15
+ import { environments } from './api/environment.js';
16
16
  import { boards, issues } from './api/work.js';
17
17
  import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './choice.js';
18
18
  import { useKept, useRead } from './data.js';
19
19
  import { useHost, useTarget } from './host.js';
20
20
  import { path } from './route.js';
21
- import { Servers } from './mcp.js';
21
+ import { Customize } from './customize/index.js';
22
22
  import { Sync } from './sync.js';
23
23
  import { grants } from './api/github.js';
24
24
  function Head({ title, says }) {
@@ -42,8 +42,9 @@ export function Forge({ screen }) {
42
42
  return _jsx(Automations, {});
43
43
  if (screen === 'sync')
44
44
  return _jsx(Sync, {});
45
+ // The fleet's native servers are Connectors → Discover now; the old address lands there.
45
46
  if (screen === 'mcp')
46
- return _jsx(Servers, {});
47
+ return _jsx(Customize, { tab: "connectors", view: "discover" });
47
48
  if (screen === 'codebases')
48
49
  return _jsx(Codebases, {});
49
50
  if (screen === 'projects')
@@ -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", 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] }), _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: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { 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);
package/lib/git.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export declare function Git({ session, title, live }: {
2
+ session: string;
3
+ title: string;
4
+ live: boolean;
5
+ }): import("react").JSX.Element;
6
+ /** A unified diff's lines, coloured by what each does: the Git tab's, and a transcript's edit card's. */
7
+ export declare function Patch({ text }: {
8
+ text: string;
9
+ }): import("react").JSX.Element;
package/lib/git.js ADDED
@@ -0,0 +1,74 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A run's Git tab: what it pushed, read from the forge. Diff is the net change
4
+ * from its base, Review is the pull request it opened and what reviewers said,
5
+ * Commits is its branch's history since the base. While the run works it is
6
+ * read again every little while, because a run pushes when it finishes a step.
7
+ */
8
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
9
+ import { ChevronDown, ChevronRight } from '@hanzogui/lucide-icons-2';
10
+ import { useEffect, useState } from 'react';
11
+ import { read } from './api/changes.js';
12
+ import { useRead } from './data.js';
13
+ import { useHost, useTarget } from './host.js';
14
+ import { Out } from './out.js';
15
+ const NONE = { repo: '', base: '', head: '', commits: [], files: [], pull: null };
16
+ const AGAIN = 15_000;
17
+ const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace: 'pre' };
18
+ export function Git({ session, title, live }) {
19
+ const host = useHost();
20
+ const t = useTarget();
21
+ const signed = Boolean(host.person);
22
+ const [view, setView] = useState('diff');
23
+ const got = useRead(signed ? () => read(t, session) : null, NONE, [t, session, signed, live]);
24
+ const { reload } = got;
25
+ useEffect(() => {
26
+ if (!live)
27
+ return;
28
+ const every = setInterval(reload, AGAIN);
29
+ return () => clearInterval(every);
30
+ }, [live, reload]);
31
+ const c = got.value;
32
+ const added = c.files.reduce((n, f) => n + f.additions, 0);
33
+ const removed = c.files.reduce((n, f) => n + f.deletions, 0);
34
+ return (_jsxs(YStack, { gap: "$3", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: title }), c.head ? (_jsxs(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [`${c.head} → ${c.base}`, c.files.length ? ` · ${c.files.length} ${c.files.length === 1 ? 'file' : 'files'} +${added} −${removed}` : ''] })) : null] }), _jsx(XStack, { gap: "$1", children: ['diff', 'review', 'commits'].map((v) => (_jsx(XStack, { render: "button", "aria-label": label(v), onPress: () => setView(v), px: "$2.5", py: "$1", rounded: "$2", bg: view === v ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: view === v ? '$ink' : '$soft', children: label(v) }) }, v))) }), !signed ? (_jsx(Soft, { children: "Sign in to see what this run pushed." })) : got.error ? (_jsx(Soft, { children: got.error.message })) : got.loading && !c.head ? (_jsx(Soft, { children: "Reading the branch\u2026" })) : view === 'diff' ? (c.files.length ? (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: c.files.map((f, i) => (_jsx(File, { change: f, first: i === 0 }, f.path))) })) : (_jsx(Soft, { children: "No pushed changes" }))) : view === 'review' ? (c.pull ? (_jsxs(YStack, { gap: "$3", children: [_jsxs(YStack, { gap: "$1.5", p: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `#${c.pull.number} ${c.pull.title}` }), _jsx(SizableText, { size: "$1", color: "$soft", children: c.pull.state === 'open'
35
+ ? c.pull.mergeable === false
36
+ ? 'Open · conflicts with its base'
37
+ : c.pull.mergeable
38
+ ? 'Open · ready to merge'
39
+ : 'Open'
40
+ : c.pull.state === 'merged'
41
+ ? 'Merged'
42
+ : 'Closed' }), c.pull.url.startsWith('https://') ? (_jsx(Out, { href: c.pull.url, label: `Open pull request #${c.pull.number}`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Open on the forge" }) })) : null] }), c.pull.reviews.length ? (c.pull.reviews.map((r, i) => (_jsxs(YStack, { gap: "$1", pb: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `${r.author || 'Someone'} · ${verdict(r.state)}${r.at ? ` · ${when(r.at)}` : ''}` }), r.body ? (_jsx(SizableText, { size: "$2", color: "$ink", children: r.body })) : null] }, i)))) : (_jsx(Soft, { children: "No reviews yet" }))] })) : (_jsx(Soft, { children: "No pull request yet. The run opens one when it pushes its changes." }))) : c.commits.length ? (_jsx(YStack, { children: c.commits.map((k) => (_jsxs(XStack, { gap: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: k.sha.slice(0, 7) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 2, children: k.message }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [k.author, when(k.date)].filter(Boolean).join(' · ') })] })] }, k.sha))) })) : (_jsx(Soft, { children: "No pushed commits" }))] }));
43
+ }
44
+ function File({ change: f, first }) {
45
+ const [open, setOpen] = useState(false);
46
+ const lines = f.patch ? f.patch.split('\n') : [];
47
+ return (_jsxs(YStack, { borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { render: "button", "aria-label": `${open ? 'Hide' : 'Show'} ${f.path}`, onPress: () => setOpen(!open), items: "center", gap: "$2", px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: [open ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$1", color: "$soft", width: 12, children: f.status[0].toUpperCase() }), _jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: f.from ? `${f.from} → ${f.path}` : f.path }), _jsx(SizableText, { size: "$1", color: "$green10", children: `+${f.additions}` }), _jsx(SizableText, { size: "$1", color: "$red10", children: `−${f.deletions}` })] }), open ? (_jsxs(YStack, { px: "$3", pb: "$2", overflow: "scroll", children: [lines.length ? (_jsx(Patch, { text: f.patch })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: f.truncated ? 'Too large to show here.' : 'Binary file' })), lines.length && f.truncated ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The rest of this file\u2019s change is too large to show here." })) : null] })) : null] }));
48
+ }
49
+ /** A unified diff's lines, coloured by what each does: the Git tab's, and a transcript's edit card's. */
50
+ export function Patch({ text }) {
51
+ return (_jsx(_Fragment, { children: text.split('\n').map((l, i) => (_jsx(SizableText, { size: "$1", style: mono, color: l.startsWith('@@') ? '$soft' : l.startsWith('+') ? '$green10' : l.startsWith('-') ? '$red10' : '$ink', children: l || ' ' }, i))) }));
52
+ }
53
+ function label(v) {
54
+ return v === 'diff' ? 'Diff' : v === 'review' ? 'Review' : 'Commits';
55
+ }
56
+ function verdict(state) {
57
+ switch (state.toUpperCase()) {
58
+ case 'APPROVED':
59
+ return 'approved';
60
+ case 'REQUEST_CHANGES':
61
+ return 'asked for changes';
62
+ case 'COMMENT':
63
+ return 'commented';
64
+ default:
65
+ return state.toLowerCase() || 'reviewed';
66
+ }
67
+ }
68
+ function when(iso) {
69
+ const d = new Date(iso);
70
+ return Number.isNaN(d.getTime()) ? '' : d.toLocaleString();
71
+ }
72
+ function Soft({ children }) {
73
+ return (_jsx(YStack, { py: "$6", items: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }) }));
74
+ }
package/lib/host.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
11
  import type { Target } from './api/call.ts';
12
+ import type { Theme } from './api/pref.ts';
12
13
  export interface Person {
13
14
  name: string;
14
15
  email: string;
@@ -26,6 +27,10 @@ export interface Host {
26
27
  memberships?: readonly string[];
27
28
  /** Stay on this page and scope every read to `org`. */
28
29
  chooseOrg?: (org: string) => void;
30
+ /** The page's theme as last chosen, for a host that can change it. */
31
+ theme?: Theme;
32
+ /** Paint the page in `theme`. A host without it keeps its own, and Settings offers no choice. */
33
+ chooseTheme?: (theme: Theme) => void;
29
34
  /** Who is signed in, or null. */
30
35
  person: Person | null;
31
36
  /** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
package/lib/landing.d.ts CHANGED
@@ -8,6 +8,16 @@ export declare const MODES: readonly [{
8
8
  readonly label: 'Plan';
9
9
  readonly hint: 'Plans the change and writes nothing';
10
10
  }];
11
+ export declare const EFFORTS: readonly [{
12
+ readonly id: 'low';
13
+ readonly label: 'Low';
14
+ }, {
15
+ readonly id: 'medium';
16
+ readonly label: 'Medium';
17
+ }, {
18
+ readonly id: 'high';
19
+ readonly label: 'High';
20
+ }];
11
21
  export declare function Landing({ onStarted }: {
12
22
  onStarted: (session: string) => void;
13
23
  }): import("react").JSX.Element;