@hanzo/build 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/builder.js CHANGED
@@ -2,11 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * The builder: the rail, and whichever pane the address names.
4
4
  *
5
- * / New — the empty state, a composer over the repo it works on
5
+ * / New — the empty state, a composer over the codebase it works on
6
6
  * /sess_… one run, live
7
+ * /-/automations repeating work
8
+ /-/codebases the forge's repositories
9
+ * /-/sync bring granted repositories onto the forge
10
+ * /-/projects the forge's boards
11
+ * /-/issues the forge's issues
7
12
  * /-/artifacts what this org has built
8
13
  * /-/templates the public starters
9
- * /<slug> a project's workspace, in this same window
14
+ * /<slug> a deployed project's workspace, in this same window
10
15
  *
11
16
  * The rail is the sessions rail every Hanzo surface shares: New, the builder's
12
17
  * places, the org's coding runs newest first with a live status dot, and the
@@ -20,12 +25,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
25
  * left column, never two.
21
26
  */
22
27
  import { SizableText, XStack, YStack } from '@hanzo/gui';
23
- import { Blocks, BookOpen, Cpu, LayoutTemplate, Menu, Puzzle } from '@hanzogui/lucide-icons-2';
28
+ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu, Puzzle, Workflow } from '@hanzogui/lucide-icons-2';
24
29
  import { Button } from '@hanzo/ui';
25
30
  import { SessionRail } from '@hanzo/ui/chat';
26
31
  import { HanzoMark } from '@hanzo/ui/product';
27
32
  import { useMemo, useState } from 'react';
33
+ import { pinBoard } from './choice.js';
28
34
  import { useKept, useRecents } from './data.js';
35
+ import { Forge } from './forge.js';
29
36
  import { HostProvider, useHost, useTarget } from './host.js';
30
37
  import { Landing } from './landing.js';
31
38
  import { Project } from './project.js';
@@ -56,6 +63,19 @@ function Shell() {
56
63
  }, [recents.value, order]);
57
64
  const screen = r.kind === 'screen' ? r.screen : '';
58
65
  const links = [
66
+ { id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' },
67
+ { id: 'codebases', label: 'Codebase', icon: _jsx(FolderGit2, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), active: screen === 'codebases' },
68
+ { id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
69
+ {
70
+ id: 'issues',
71
+ label: 'Issues',
72
+ icon: _jsx(CircleDot, { size: 16 }),
73
+ onPress: () => {
74
+ pinBoard(host.org, '');
75
+ go(path({ kind: 'screen', screen: 'issues' }));
76
+ },
77
+ active: screen === 'issues',
78
+ },
59
79
  { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
60
80
  { id: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16 }), onPress: () => host.open(host.links.customize) },
61
81
  ];
@@ -64,9 +84,9 @@ function Shell() {
64
84
  { id: 'machines', label: 'Machines', icon: _jsx(Cpu, { size: 16 }), onPress: () => host.open(`${host.links.home.replace(/\/+$/, '')}/platform/computers`) },
65
85
  { id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open('https://docs.hanzo.ai/docs/dev', '_blank', 'noopener,noreferrer') },
66
86
  ];
67
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
68
- ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
69
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
87
+ return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
88
+ ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
89
+ : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
70
90
  }
71
91
  /** Whichever pane the address names, beside a rail — the builder's or the host's. */
72
92
  function Pane({ onStarted }) {
@@ -74,13 +94,25 @@ function Pane({ onStarted }) {
74
94
  const r = route(host.path);
75
95
  if (r.kind === 'run')
76
96
  return _jsx(Run, { id: r.id }, r.id);
77
- if (r.kind === 'screen')
78
- return r.screen === 'artifacts' ? _jsx(Artifacts, {}) : _jsx(Templates, {});
97
+ if (r.kind === 'screen') {
98
+ if (r.screen === 'artifacts')
99
+ return _jsx(Artifacts, {});
100
+ if (r.screen === 'templates')
101
+ return _jsx(Templates, {});
102
+ return _jsx(Forge, { screen: r.screen }, r.screen);
103
+ }
79
104
  return (_jsx(Landing, { onStarted: (id) => {
80
105
  onStarted?.(id);
81
106
  host.go(id);
82
107
  } }));
83
108
  }
109
+ /** The Hanzo mark on a personal account. An organization shows its own initial. */
110
+ function Brand({ org }) {
111
+ if (!org || org === 'hanzo')
112
+ return _jsx(HanzoMark, { size: 18 });
113
+ const letter = org.trim().charAt(0).toUpperCase() || 'H';
114
+ return (_jsx(XStack, { width: 18, height: 18, rounded: "$1", items: "center", justify: "center", bg: "$raised", "aria-hidden": true, children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: letter }) }));
115
+ }
84
116
  function Screens({ rail }) {
85
117
  const host = useHost();
86
118
  const r = route(host.path);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The choice New starts from, kept in this browser so a codebase or an issue
3
+ * opened from its own screen is what the composer is holding when it mounts.
4
+ *
5
+ * The key is the one `useKept` in New writes. A merge keeps the mode, the
6
+ * model and the machine; it only replaces the fields the caller names.
7
+ */
8
+ import { type Codebase, type ForgeRepo } from './api/codebases.ts';
9
+ export interface Choice {
10
+ repo: ForgeRepo | null;
11
+ branch: string;
12
+ place: string;
13
+ mode: string;
14
+ model: string;
15
+ effort: string;
16
+ /** A draft handed over by another screen. New copies it once and clears it. */
17
+ ask: string;
18
+ }
19
+ export declare const boardKey: (org: string | null) => string;
20
+ /** Point New at a forge codebase, and optionally a first draft. */
21
+ export declare function pinCodebase(org: string | null, c: Codebase, ask?: string): void;
22
+ /** Which board Issues is showing. '' is every board. */
23
+ export declare function pinBoard(org: string | null, key: string): void;
24
+ export declare const isForge: (repo: {
25
+ forge?: boolean;
26
+ name?: string;
27
+ } | null) => repo is ForgeRepo;
28
+ /** A repository queued onto the forge and not reported landed yet. */
29
+ export interface Pending {
30
+ fullName: string;
31
+ name: string;
32
+ }
33
+ export declare function readPending(org: string | null): Pending[];
34
+ export declare function writePending(org: string | null, rows: Pending[]): void;
package/lib/choice.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The choice New starts from, kept in this browser so a codebase or an issue
3
+ * opened from its own screen is what the composer is holding when it mounts.
4
+ *
5
+ * The key is the one `useKept` in New writes. A merge keeps the mode, the
6
+ * model and the machine; it only replaces the fields the caller names.
7
+ */
8
+ import { asRepo } from './api/codebases.js';
9
+ const KEY = (org) => `hanzo.build.new.${org ?? 'none'}`;
10
+ export const boardKey = (org) => `hanzo.build.board.${org ?? 'none'}`;
11
+ function read(org) {
12
+ try {
13
+ const raw = window.localStorage.getItem(KEY(org));
14
+ const v = raw ? JSON.parse(raw) : null;
15
+ return v && typeof v === 'object' ? v : {};
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ function write(org, value) {
22
+ try {
23
+ window.localStorage.setItem(KEY(org), JSON.stringify(value));
24
+ }
25
+ catch {
26
+ /* the next New starts empty */
27
+ }
28
+ }
29
+ /** Point New at a forge codebase, and optionally a first draft. */
30
+ export function pinCodebase(org, c, ask = '') {
31
+ const repo = asRepo(c);
32
+ write(org, { ...read(org), repo, branch: repo.default_branch || 'main', ask });
33
+ }
34
+ /** Which board Issues is showing. '' is every board. */
35
+ export function pinBoard(org, key) {
36
+ try {
37
+ window.localStorage.setItem(boardKey(org), JSON.stringify(key));
38
+ }
39
+ catch {
40
+ /* Issues then shows every board */
41
+ }
42
+ window.dispatchEvent(new Event('hanzo-board'));
43
+ }
44
+ export const isForge = (repo) => Boolean(repo && repo.forge === true && repo.name);
45
+ const syncingKey = (org) => `hanzo.build.sync.${org ?? 'none'}`;
46
+ export function readPending(org) {
47
+ try {
48
+ const raw = window.sessionStorage.getItem(syncingKey(org));
49
+ const v = raw ? JSON.parse(raw) : [];
50
+ if (!Array.isArray(v))
51
+ return [];
52
+ return v.filter((p) => Boolean(p) && typeof p === 'object' && typeof p.fullName === 'string' && p.fullName !== '' && typeof p.name === 'string' && p.name !== '');
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ export function writePending(org, rows) {
59
+ try {
60
+ if (rows.length === 0)
61
+ window.sessionStorage.removeItem(syncingKey(org));
62
+ else
63
+ window.sessionStorage.setItem(syncingKey(org), JSON.stringify(rows));
64
+ }
65
+ catch {
66
+ /* the table then omits the in-flight rows */
67
+ }
68
+ }
package/lib/data.js CHANGED
@@ -172,6 +172,7 @@ export function useRun(t, id) {
172
172
  title: pushed.title || base.title,
173
173
  branch: pushed.branch || base.branch,
174
174
  pr: pushed.pr || base.pr,
175
+ mode: pushed.mode || base.mode,
175
176
  };
176
177
  }, [read, frame]);
177
178
  return { detail, record, events, status: record?.status ?? '', refused };
package/lib/desk.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { Event } from './api/sessions.ts';
2
+ export declare function Desk({ id, repo, branch, base, environment, mode, pr, events, live, refused, onRetry, onHide, }: {
3
+ id: string;
4
+ repo: string;
5
+ branch: string;
6
+ base: string;
7
+ environment: string;
8
+ mode: string;
9
+ pr: {
10
+ href: string;
11
+ label: string;
12
+ };
13
+ events: Event[];
14
+ live: boolean;
15
+ refused: string;
16
+ onRetry: () => void;
17
+ onHide?: () => void;
18
+ }): import("react").JSX.Element;
package/lib/desk.js ADDED
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The pane beside a run's transcript: the machine it runs on, the branch it
4
+ * writes, the commands it ran, and the files on that branch.
5
+ */
6
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
7
+ import { Copy, File, Folder, MoreHorizontal, PanelRightClose } from '@hanzogui/lucide-icons-2';
8
+ import { Button, DropdownMenu } from '@hanzo/ui';
9
+ import { useState } from 'react';
10
+ import { blob, tree } from './api/git.js';
11
+ import { shell } from './api/turn.js';
12
+ import { useRead } from './data.js';
13
+ import { useTarget } from './host.js';
14
+ import { Out } from './out.js';
15
+ export function Desk({ id, repo, branch, base, environment, mode, pr, events, live, refused, onRetry, onHide, }) {
16
+ const [tab, setTab] = useState('terminal');
17
+ const [copied, setCopied] = useState(false);
18
+ const name = repo.split('/').filter(Boolean).pop() || repo;
19
+ const ref = branch || base || 'main';
20
+ const lines = shell(events);
21
+ const copy = async () => {
22
+ try {
23
+ await navigator.clipboard.writeText(id);
24
+ setCopied(true);
25
+ }
26
+ catch {
27
+ setCopied(false);
28
+ }
29
+ };
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: [
31
+ { key: 'copy', label: copied ? 'Copied' : 'Copy run id', icon: _jsx(Copy, { size: 16 }), description: id, onSelect: () => void copy() },
32
+ { key: 'details', label: 'Details', onSelect: () => setTab('environment') },
33
+ ] })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: tab === 'terminal' ? 'hidden' : 'scroll', p: tab === 'terminal' ? 0 : undefined, px: tab === 'terminal' ? 0 : '$3', py: tab === 'terminal' ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_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' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Fact, { label: "Writing", value: branch || '—' }), _jsx(Fact, { label: "From", value: base || '—' }), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Pull request ", pr.label] }) })) : (_jsx(Fact, { label: "Pull request", value: "None yet" }))] })) : null, tab === 'terminal' ? (_jsx(Terminal, { lines: lines, live: live, refused: refused, onRetry: onRetry })) : null, tab === 'files' ? _jsx(Files, { repo: name, refName: ref }) : 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
+ }
35
+ function TabButton({ id, tab, onPick, children }) {
36
+ const on = tab === id;
37
+ return (_jsx(XStack, { render: "button", "aria-label": children, onPress: () => onPick(id), px: "$2", py: "$2", borderBottomWidth: 2, borderColor: on ? '$ink' : 'transparent', children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', children: children }) }));
38
+ }
39
+ function Fact({ label, value }) {
40
+ 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
+ }
42
+ const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace: 'pre' };
43
+ function Terminal({ lines, live, refused, onRetry, }) {
44
+ const prompt = 'workspace';
45
+ const idle = lines.length === 0 && !live;
46
+ return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsx(XStack, { px: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Terminal 1" }) }), _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." }) })] }));
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] }));
56
+ }
package/lib/forge.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Screen } from './route.ts';
2
+ export declare function Forge({ screen }: {
3
+ screen: Exclude<Screen, 'artifacts' | 'templates'>;
4
+ }): import("react").JSX.Element;
package/lib/forge.js ADDED
@@ -0,0 +1,266 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Codebases, projects and issues, read from the forge and opened in this window.
4
+ *
5
+ * A codebase is a repository. Choosing one is choosing what the next run works
6
+ * on. A project is a board of issues on one of those repositories. Choosing an
7
+ * issue opens New with that codebase and the issue as the ask.
8
+ */
9
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
+ import { ChevronDown, GitBranch, Plus, Settings, Workflow } from '@hanzogui/lucide-icons-2';
11
+ import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
12
+ import { useEffect, useState } from 'react';
13
+ import { add, arm, flows } from './api/auto.js';
14
+ import { codebases, create } from './api/codebases.js';
15
+ import { boards, issues } from './api/work.js';
16
+ import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './choice.js';
17
+ import { useKept, useRead } from './data.js';
18
+ import { useHost, useTarget } from './host.js';
19
+ import { path } from './route.js';
20
+ import { Sync } from './sync.js';
21
+ import { grants } from './api/github.js';
22
+ function Head({ title, says }) {
23
+ return (_jsxs(YStack, { gap: "$1", pb: "$4", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }));
24
+ }
25
+ function Row({ label, onPress, children }) {
26
+ return (_jsx(XStack, { render: "button", onPress: onPress, "aria-label": label, items: "center", gap: "$3", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: children }));
27
+ }
28
+ function Note({ children }) {
29
+ return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
30
+ }
31
+ const STATUS = {
32
+ backlog: 'Backlog',
33
+ todo: 'To do',
34
+ in_progress: 'In progress',
35
+ done: 'Done',
36
+ canceled: 'Canceled',
37
+ };
38
+ export function Forge({ screen }) {
39
+ if (screen === 'automations')
40
+ return _jsx(Automations, {});
41
+ if (screen === 'sync')
42
+ return _jsx(Sync, {});
43
+ if (screen === 'codebases')
44
+ return _jsx(Codebases, {});
45
+ if (screen === 'projects')
46
+ return _jsx(Projects, {});
47
+ return _jsx(Issues, {});
48
+ }
49
+ function Automations() {
50
+ const host = useHost();
51
+ const t = useTarget();
52
+ const signed = Boolean(host.person);
53
+ const [q, setQ] = useState('');
54
+ const [making, setMaking] = useState(false);
55
+ const [tick, setTick] = useState(0);
56
+ const [problem, setProblem] = useState('');
57
+ const list = useRead(signed ? () => flows(t) : null, [], [t, signed, tick]);
58
+ const needle = q.trim().toLowerCase();
59
+ const shown = list.value.filter((a) => !needle || a.name.toLowerCase().includes(needle));
60
+ const flip = async (a) => {
61
+ setProblem('');
62
+ try {
63
+ await arm(t, a.id, a.status !== 'ENABLED');
64
+ setTick((n) => n + 1);
65
+ }
66
+ catch (e) {
67
+ setProblem(e instanceof Error ? e.message : 'Could not change that automation');
68
+ }
69
+ };
70
+ 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) => {
71
+ await add(t, name);
72
+ setMaking(false);
73
+ setTick((n) => n + 1);
74
+ } })] }));
75
+ }
76
+ function Empty({ says, children }) {
77
+ return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$4", px: "$4", py: "$8", gap: "$3", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: says }), children] }));
78
+ }
79
+ function NewAutomation({ open, onOpenChange, onCreate, }) {
80
+ const [name, setName] = useState('');
81
+ const [busy, setBusy] = useState(false);
82
+ const [error, setError] = useState('');
83
+ const submit = async () => {
84
+ setBusy(true);
85
+ setError('');
86
+ try {
87
+ await onCreate(name);
88
+ setName('');
89
+ }
90
+ catch (e) {
91
+ setError(e instanceof Error ? e.message : 'Could not create that automation');
92
+ }
93
+ finally {
94
+ setBusy(false);
95
+ }
96
+ };
97
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogTitle, { children: "New automation" }), _jsxs(YStack, { gap: "$3", pt: "$3", children: [_jsx(Input, { value: name, onChangeText: setName, placeholder: "Name", "aria-label": "Automation name" }), error ? _jsx(Note, { children: error }) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] })] }) }));
98
+ }
99
+ const PAGE = 25;
100
+ function Codebases() {
101
+ const host = useHost();
102
+ const t = useTarget();
103
+ const signed = Boolean(host.person);
104
+ const [q, setQ] = useState('');
105
+ const [making, setMaking] = useState(false);
106
+ const [sort, setSort] = useState('name');
107
+ const [page, setPage] = useState(0);
108
+ const [landed, setLanded] = useState(0);
109
+ useEffect(() => {
110
+ setPage(0);
111
+ }, [q, sort]);
112
+ const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, making, landed]);
113
+ const needle = q.trim().toLowerCase();
114
+ const pending = readPending(host.org);
115
+ const pendingKey = pending.map((p) => p.fullName).join('\n');
116
+ const syncing = new Set(pending.map((p) => p.name));
117
+ useEffect(() => {
118
+ const waiting = readPending(host.org);
119
+ if (!signed || waiting.length === 0)
120
+ return;
121
+ let stop = false;
122
+ grants(t)
123
+ .then((g) => {
124
+ if (stop)
125
+ return;
126
+ const done = new Set(g.repos.filter((r) => r.imported).map((r) => r.fullName));
127
+ const left = waiting.filter((p) => !done.has(p.fullName));
128
+ if (left.length !== waiting.length) {
129
+ writePending(host.org, left);
130
+ setLanded((n) => n + 1);
131
+ }
132
+ })
133
+ .catch(() => { });
134
+ return () => {
135
+ stop = true;
136
+ };
137
+ }, [signed, t, host.org, pendingKey]);
138
+ const shown = [
139
+ ...list.value,
140
+ ...pending
141
+ .filter((p) => !list.value.some((c) => c.name === p.name))
142
+ .map((p) => ({
143
+ org: host.org ?? '',
144
+ name: p.name,
145
+ description: '',
146
+ branch: 'main',
147
+ public: false,
148
+ clone: '',
149
+ updated: '',
150
+ branches: ['main'],
151
+ })),
152
+ ]
153
+ .filter((c) => !needle || `${c.org}/${c.name} ${c.description}`.toLowerCase().includes(needle))
154
+ .slice()
155
+ .sort((a, b) => {
156
+ if (sort === 'updated') {
157
+ const d = Date.parse(b.updated) - Date.parse(a.updated);
158
+ if (Number.isFinite(d) && d !== 0)
159
+ return d;
160
+ }
161
+ return a.name.localeCompare(b.name);
162
+ });
163
+ const pages = Math.max(1, Math.ceil(shown.length / PAGE));
164
+ const safePage = Math.min(page, pages - 1);
165
+ const slice = shown.slice(safePage * PAGE, safePage * PAGE + PAGE);
166
+ const from = shown.length === 0 ? 0 : safePage * PAGE + 1;
167
+ const to = safePage * PAGE + slice.length;
168
+ const open = (c) => {
169
+ pinCodebase(host.org, c);
170
+ host.go('');
171
+ };
172
+ 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: () => host.open(host.links.settings), 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) => {
173
+ const row = await create(t, name, description);
174
+ pinCodebase(host.org, row);
175
+ setMaking(false);
176
+ host.go('');
177
+ } })] }));
178
+ }
179
+ function NewRepo({ open, onOpenChange, onCreate, }) {
180
+ const [name, setName] = useState('');
181
+ const [description, setDescription] = useState('');
182
+ const [busy, setBusy] = useState(false);
183
+ const [error, setError] = useState('');
184
+ const submit = async () => {
185
+ setBusy(true);
186
+ setError('');
187
+ try {
188
+ await onCreate(name, description);
189
+ setName('');
190
+ setDescription('');
191
+ }
192
+ catch (e) {
193
+ setError(e instanceof Error ? e.message : 'Could not create that repository');
194
+ }
195
+ finally {
196
+ setBusy(false);
197
+ }
198
+ };
199
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogTitle, { children: "New repository" }), _jsxs(YStack, { gap: "$3", pt: "$3", children: [_jsx(Input, { value: name, onChangeText: setName, placeholder: "Name", "aria-label": "Repository name" }), _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "Description", "aria-label": "Description" }), error ? _jsx(Note, { children: error }) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] })] }) }));
200
+ }
201
+ function when(iso) {
202
+ const t = Date.parse(iso);
203
+ if (!Number.isFinite(t))
204
+ return '—';
205
+ const minutes = Math.round((Date.now() - t) / 60000);
206
+ if (minutes < 1)
207
+ return 'just now';
208
+ if (minutes < 60)
209
+ return `${minutes}m ago`;
210
+ const hours = Math.round(minutes / 60);
211
+ if (hours < 24)
212
+ return `${hours}h ago`;
213
+ const days = Math.round(hours / 24);
214
+ if (days < 30)
215
+ return `${days}d ago`;
216
+ return new Date(t).toLocaleDateString();
217
+ }
218
+ function Projects() {
219
+ const host = useHost();
220
+ const t = useTarget();
221
+ const signed = Boolean(host.person);
222
+ const list = useRead(signed ? () => boards(t) : null, [], [t, signed]);
223
+ const open = (b) => {
224
+ pinBoard(host.org, b.key);
225
+ host.go(path({ kind: 'screen', screen: 'issues' }));
226
+ };
227
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Projects", says: "Boards on the forge. A board is a repository that has work on it." }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's boards." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsx(Note, { children: "No boards yet. A repository shows up here once it has an issue." })) : (_jsx(YStack, { gap: "$2", children: list.value.map((b) => (_jsx(Row, { label: `Open ${b.name}`, onPress: () => open(b), children: _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: b.name }), b.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: b.description })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: b.key }))] }) }, b.key))) }))] }) }));
228
+ }
229
+ function Issues() {
230
+ const host = useHost();
231
+ const t = useTarget();
232
+ const signed = Boolean(host.person);
233
+ const [board, setBoard] = useKept(boardKey(host.org), '');
234
+ const list = useRead(signed ? () => issues(t, board) : null, [], [t, signed, board]);
235
+ useEffect(() => {
236
+ const on = () => {
237
+ try {
238
+ const raw = window.localStorage.getItem(boardKey(host.org));
239
+ const next = raw === null ? '' : JSON.parse(raw);
240
+ setBoard(typeof next === 'string' ? next : '');
241
+ }
242
+ catch {
243
+ setBoard('');
244
+ }
245
+ };
246
+ window.addEventListener('hanzo-board', on);
247
+ return () => window.removeEventListener('hanzo-board', on);
248
+ }, [host.org, setBoard]);
249
+ const open = (w) => {
250
+ const name = w.repo || w.project;
251
+ if (name) {
252
+ pinCodebase(host.org, {
253
+ org: host.org ?? '',
254
+ name,
255
+ description: '',
256
+ branch: 'main',
257
+ public: false,
258
+ clone: '',
259
+ updated: '',
260
+ branches: ['main'],
261
+ }, `${w.identifier ? `${w.identifier} ` : ''}${w.title}`.trim());
262
+ }
263
+ host.go('');
264
+ };
265
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Issues", says: board ? `Open work on ${board}. Choosing one starts the next run on that codebase.` : 'Open work across every board. Choosing one starts the next run on that codebase.' }), board ? (_jsx(XStack, { render: "button", onPress: () => setBoard(''), "aria-label": "Show every board", self: "flex-start", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "All boards" }) })) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsx(Note, { children: "Nothing open." })) : (_jsx(YStack, { gap: "$2", children: list.value.map((w) => (_jsxs(Row, { label: `Build ${w.identifier || w.title}`, onPress: () => open(w), children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 88, numberOfLines: 1, children: w.identifier || w.kind }), _jsx(SizableText, { size: "$3", color: "$ink", flex: 1, minW: 0, numberOfLines: 1, children: w.title }), _jsx(SizableText, { size: "$1", color: "$soft", children: STATUS[w.status] ?? w.status })] }, w.id))) }))] }) }));
266
+ }
package/lib/host.d.ts CHANGED
@@ -26,7 +26,7 @@ export interface Host {
26
26
  person: Person | null;
27
27
  /** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
28
28
  admin: boolean;
29
- /** The address under the mount: '' | a session id | a project slug | '-/<screen>'. */
29
+ /** The address under the mount: '' | a session id | a slug | '-/codebases' | '-/projects' | '-/issues' | '-/artifacts' | '-/templates'. */
30
30
  path: string;
31
31
  /** Move under the mount. */
32
32
  go: (path: string, how?: {