@hanzo/build 0.2.0

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 (57) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/NOTICE +37 -0
  4. package/README.md +120 -0
  5. package/lib/account.d.ts +11 -0
  6. package/lib/account.js +26 -0
  7. package/lib/api/call.d.ts +38 -0
  8. package/lib/api/call.js +75 -0
  9. package/lib/api/coding.d.ts +56 -0
  10. package/lib/api/coding.js +58 -0
  11. package/lib/api/git.d.ts +27 -0
  12. package/lib/api/git.js +39 -0
  13. package/lib/api/github.d.ts +78 -0
  14. package/lib/api/github.js +73 -0
  15. package/lib/api/models.d.ts +17 -0
  16. package/lib/api/models.js +28 -0
  17. package/lib/api/places.d.ts +25 -0
  18. package/lib/api/places.js +31 -0
  19. package/lib/api/platform.d.ts +59 -0
  20. package/lib/api/platform.js +69 -0
  21. package/lib/api/projects.d.ts +52 -0
  22. package/lib/api/projects.js +97 -0
  23. package/lib/api/sessions.d.ts +74 -0
  24. package/lib/api/sessions.js +120 -0
  25. package/lib/api/sse.d.ts +24 -0
  26. package/lib/api/sse.js +68 -0
  27. package/lib/api/turn.d.ts +51 -0
  28. package/lib/api/turn.js +121 -0
  29. package/lib/api/verdict.d.ts +12 -0
  30. package/lib/api/verdict.js +20 -0
  31. package/lib/builder.d.ts +5 -0
  32. package/lib/builder.js +81 -0
  33. package/lib/data.d.ts +42 -0
  34. package/lib/data.js +178 -0
  35. package/lib/host.d.ts +57 -0
  36. package/lib/host.js +26 -0
  37. package/lib/index.d.ts +11 -0
  38. package/lib/index.js +10 -0
  39. package/lib/landing.d.ts +13 -0
  40. package/lib/landing.js +132 -0
  41. package/lib/out.d.ts +10 -0
  42. package/lib/out.js +4 -0
  43. package/lib/project.d.ts +3 -0
  44. package/lib/project.js +250 -0
  45. package/lib/publish.d.ts +15 -0
  46. package/lib/publish.js +94 -0
  47. package/lib/route.d.ts +30 -0
  48. package/lib/route.js +28 -0
  49. package/lib/run.d.ts +3 -0
  50. package/lib/run.js +67 -0
  51. package/lib/shelf.d.ts +2 -0
  52. package/lib/shelf.js +60 -0
  53. package/lib/tools.d.ts +25 -0
  54. package/lib/tools.js +64 -0
  55. package/lib/voice.d.ts +17 -0
  56. package/lib/voice.js +86 -0
  57. package/package.json +70 -0
package/lib/data.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The builder's reads, as hooks: one per thing on screen, each answering
3
+ * `{value, error, loading, reload}` so every pane owes the same three states.
4
+ *
5
+ * A read that fails says so and keeps what it had; a read with no person
6
+ * signed in is not attempted — the platform would refuse it and the refusal
7
+ * would read as an outage.
8
+ */
9
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
+ import { places } from './api/places.js';
11
+ import { projects } from './api/projects.js';
12
+ import { get, list, watch } from './api/sessions.js';
13
+ import { merge } from './api/turn.js';
14
+ /** Run `load` whenever `key` changes (and on `reload`), keeping the last good value. */
15
+ export function useRead(load, initial, key) {
16
+ const [value, setValue] = useState(initial);
17
+ const [error, setError] = useState(null);
18
+ const [loading, setLoading] = useState(Boolean(load));
19
+ const [tick, setTick] = useState(0);
20
+ const reload = useCallback(() => setTick((n) => n + 1), []);
21
+ useEffect(() => {
22
+ if (!load) {
23
+ setLoading(false);
24
+ return;
25
+ }
26
+ let live = true;
27
+ setLoading(true);
28
+ load()
29
+ .then((v) => {
30
+ if (!live)
31
+ return;
32
+ setValue(v);
33
+ setError(null);
34
+ })
35
+ .catch((e) => live && setError(e instanceof Error ? e : new Error(String(e))))
36
+ .finally(() => live && setLoading(false));
37
+ return () => {
38
+ live = false;
39
+ };
40
+ // `key` is the dependency list, by contract.
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [...key, tick]);
43
+ return { value, error, loading, reload };
44
+ }
45
+ /**
46
+ * The org's coding runs, newest first, kept current by the org's own session
47
+ * stream: a status that moves moves its dot, and a run started anywhere appears.
48
+ */
49
+ export function useRecents(t, signed) {
50
+ const read = useRead(signed ? () => list(t, { kind: 'coding', limit: 50 }) : null, [], [t, signed]);
51
+ const [rows, setRows] = useState([]);
52
+ useEffect(() => setRows(read.value), [read.value]);
53
+ // A feed of the whole org carries every agent's heartbeat, so an unseen id
54
+ // re-reads the list at most every five seconds.
55
+ const last = useRef(0);
56
+ const reload = useRef(() => { });
57
+ reload.current = () => {
58
+ const now = Date.now();
59
+ if (now - last.current < 5000)
60
+ return;
61
+ last.current = now;
62
+ read.reload();
63
+ };
64
+ useEffect(() => {
65
+ if (!signed)
66
+ return;
67
+ const ctl = new AbortController();
68
+ void watch(t, '', {
69
+ session: (s) => setRows((prev) => {
70
+ const i = prev.findIndex((r) => r.id === s.id);
71
+ if (i === -1) {
72
+ // A run this list has not seen: re-read rather than guess its kind.
73
+ reload.current();
74
+ return prev;
75
+ }
76
+ const next = prev.slice();
77
+ next[i] = { ...prev[i], ...s, title: s.title || prev[i].title };
78
+ return next;
79
+ }),
80
+ open: (n) => {
81
+ if (n > 0)
82
+ reload.current();
83
+ },
84
+ }, ctl.signal).catch(() => {
85
+ /* a refused feed leaves the list as read */
86
+ });
87
+ return () => ctl.abort();
88
+ }, [t, signed]);
89
+ return { ...read, value: rows };
90
+ }
91
+ export function usePlaces(t, signed) {
92
+ return useRead(signed ? () => places(t) : null, [], [t, signed]);
93
+ }
94
+ export function useProjects(t, signed) {
95
+ return useRead(signed ? () => projects(t) : null, [], [t, signed]);
96
+ }
97
+ /** A value kept in this browser, per key; storage that throws keeps it in memory. */
98
+ export function useKept(key, initial) {
99
+ const [value, setValue] = useState(() => {
100
+ try {
101
+ const raw = window.localStorage.getItem(key);
102
+ return raw === null ? initial : JSON.parse(raw);
103
+ }
104
+ catch {
105
+ return initial;
106
+ }
107
+ });
108
+ const set = useCallback((v) => {
109
+ setValue(v);
110
+ try {
111
+ window.localStorage.setItem(key, JSON.stringify(v));
112
+ }
113
+ catch {
114
+ /* kept for this page only */
115
+ }
116
+ }, [key]);
117
+ return [value, set];
118
+ }
119
+ /**
120
+ * One run: the recorded detail, then the live feed added to it. A reconnect
121
+ * after a drop re-reads the detail — frames missed while dropped are only in
122
+ * the record. `id` null reads nothing.
123
+ */
124
+ export function useRun(t, id) {
125
+ const detail = useRead(id ? () => get(t, id) : null, null, [t, id]);
126
+ const [live, setLive] = useState([]);
127
+ const [pushed, setPushed] = useState(null);
128
+ const [refused, setRefused] = useState('');
129
+ const reload = useRef(detail.reload);
130
+ reload.current = detail.reload;
131
+ useEffect(() => {
132
+ setLive([]);
133
+ setPushed(null);
134
+ setRefused('');
135
+ if (!id)
136
+ return;
137
+ const ctl = new AbortController();
138
+ void watch(t, id, {
139
+ // A frame from a stream this run has already left is not this run's.
140
+ event: (e) => {
141
+ if (!ctl.signal.aborted && e.sessionId === id)
142
+ setLive((prev) => [...prev, e]);
143
+ },
144
+ session: (s) => {
145
+ if (!ctl.signal.aborted && s.id === id)
146
+ setPushed(s);
147
+ },
148
+ open: (n) => {
149
+ if (n > 0)
150
+ reload.current();
151
+ },
152
+ }, ctl.signal).catch((e) => setRefused(e instanceof Error ? e.message : 'The feed was refused'));
153
+ return () => ctl.abort();
154
+ }, [t, id]);
155
+ // The read and the frames describe THIS run or nothing: a read kept from the
156
+ // run before (it stays until the next one lands, or forever if it fails) must
157
+ // not lend this run its pull request.
158
+ const read = detail.value && detail.value.id === id ? detail.value : null;
159
+ const frame = pushed && pushed.id === id ? pushed : null;
160
+ const events = useMemo(() => merge(read?.recent ?? [], live), [read, live]);
161
+ // A frame newer than the read wins; the read fills what a frame left empty.
162
+ const record = useMemo(() => {
163
+ const base = read;
164
+ const pushed = frame;
165
+ if (!pushed)
166
+ return base;
167
+ if (!base)
168
+ return pushed;
169
+ return {
170
+ ...base,
171
+ ...pushed,
172
+ title: pushed.title || base.title,
173
+ branch: pushed.branch || base.branch,
174
+ pr: pushed.pr || base.pr,
175
+ };
176
+ }, [read, frame]);
177
+ return { detail, record, events, status: record?.status ?? '', refused };
178
+ }
package/lib/host.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What the builder needs from the page that mounts it.
3
+ *
4
+ * The builder is ONE component with two hosts: its own page (hanzo.build, which
5
+ * signs in through Hanzo IAM and routes with react-router) and the platform
6
+ * console (platform.hanzo.ai/dev, which already has a session and a router).
7
+ * Everything that differs between them is here, as values, so neither host
8
+ * forks the builder and the builder imports neither host.
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { Target } from './api/call.ts';
12
+ export interface Person {
13
+ name: string;
14
+ email: string;
15
+ /** An https image, or '' for the initial. */
16
+ avatar: string;
17
+ }
18
+ export interface Host {
19
+ /** The platform origin, no trailing slash. */
20
+ api: string;
21
+ /** The signed-in person's bearer, or null. Read at call time, so a refresh is picked up. */
22
+ token: () => string | null;
23
+ /** The org every call is scoped to. */
24
+ org: string | null;
25
+ /** Who is signed in, or null. */
26
+ person: Person | null;
27
+ /** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
28
+ admin: boolean;
29
+ /** The address under the mount: '' | a session id | a project slug | '-/<screen>'. */
30
+ path: string;
31
+ /** Move under the mount. */
32
+ go: (path: string, how?: {
33
+ replace?: boolean;
34
+ }) => void;
35
+ /** Where the builder links out to. Same-window addresses on the host, or https. */
36
+ links: {
37
+ /** Troubleshoot GitHub connection: the GitHub pane of the platform's plugins. */
38
+ github: string;
39
+ /** Customize: the platform's plugins (skills, MCP, connectors). */
40
+ customize: string;
41
+ /** Account settings. */
42
+ settings: string;
43
+ /** The platform itself, where the mark leads. */
44
+ home: string;
45
+ };
46
+ /** Leave the builder for a host address, same window. */
47
+ open: (href: string) => void;
48
+ signIn?: () => void;
49
+ signOut?: () => void;
50
+ }
51
+ export declare function HostProvider({ host, children }: {
52
+ host: Host;
53
+ children: ReactNode;
54
+ }): import("react").JSX.Element;
55
+ export declare function useHost(): Host;
56
+ /** The call target for this host — stable while the host's api, token reader and org are. */
57
+ export declare function useTarget(): Target;
package/lib/host.js ADDED
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * What the builder needs from the page that mounts it.
4
+ *
5
+ * The builder is ONE component with two hosts: its own page (hanzo.build, which
6
+ * signs in through Hanzo IAM and routes with react-router) and the platform
7
+ * console (platform.hanzo.ai/dev, which already has a session and a router).
8
+ * Everything that differs between them is here, as values, so neither host
9
+ * forks the builder and the builder imports neither host.
10
+ */
11
+ import { createContext, useContext, useMemo } from 'react';
12
+ const Context = createContext(null);
13
+ export function HostProvider({ host, children }) {
14
+ return _jsx(Context.Provider, { value: host, children: children });
15
+ }
16
+ export function useHost() {
17
+ const h = useContext(Context);
18
+ if (!h)
19
+ throw new Error('The builder was rendered without a host');
20
+ return h;
21
+ }
22
+ /** The call target for this host — stable while the host's api, token reader and org are. */
23
+ export function useTarget() {
24
+ const { api, token, org } = useHost();
25
+ return useMemo(() => ({ api, token, org }), [api, token, org]);
26
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @hanzo/build — the builder as a component.
3
+ *
4
+ * Mount `<Builder host={…} />` under a gui root in a box with a height. The
5
+ * host says where the platform is, who is signed in, and how to move; the
6
+ * builder does everything else. hanzo.build and platform.hanzo.ai/dev are its
7
+ * two hosts.
8
+ */
9
+ export { Builder } from './builder.tsx';
10
+ export type { Host, Person } from './host.tsx';
11
+ export { path, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @hanzo/build — the builder as a component.
3
+ *
4
+ * Mount `<Builder host={…} />` under a gui root in a box with a height. The
5
+ * host says where the platform is, who is signed in, and how to move; the
6
+ * builder does everything else. hanzo.build and platform.hanzo.ai/dev are its
7
+ * two hosts.
8
+ */
9
+ export { Builder } from './builder.js';
10
+ export { path, route, SESSION, SLUG } from './route.js';
@@ -0,0 +1,13 @@
1
+ /** One vocabulary for the mode, on New and in a workspace. */
2
+ export declare const MODES: readonly [{
3
+ readonly id: 'build';
4
+ readonly label: 'Build';
5
+ readonly hint: 'Edits, commits and pushes a branch';
6
+ }, {
7
+ readonly id: 'plan';
8
+ readonly label: 'Plan';
9
+ readonly hint: 'Plans the change and writes nothing';
10
+ }];
11
+ export declare function Landing({ onStarted }: {
12
+ onStarted: (session: string) => void;
13
+ }): import("react").JSX.Element;
package/lib/landing.js ADDED
@@ -0,0 +1,132 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * New: the empty state. A heading at the top of the column, and at the foot of
4
+ * the pane the composer — where the run goes (the sandbox or one of the org's
5
+ * machines), which repository and branch it starts from, and the ask itself.
6
+ * Under it: attach, dictate, the mode, and the model and effort on the right.
7
+ *
8
+ * Sending starts a coding run and opens it. The choices a person made here are
9
+ * kept in this browser, per org, so the next New starts where the last one did.
10
+ */
11
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
12
+ import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
13
+ import { Button } from '@hanzo/ui';
14
+ import { ModeSelect } from '@hanzo/ui/agents';
15
+ import { Composer, EmptyPrompt } from '@hanzo/ui/chat';
16
+ import { BranchSelect, ChipSelect, HanzoMark, RepoSelect } from '@hanzo/ui/product';
17
+ import { useCallback, useMemo, useState } from 'react';
18
+ import { start, unhonoured } from './api/coding.js';
19
+ import { branches, connect, repos } from './api/github.js';
20
+ import { ENSO, models } from './api/models.js';
21
+ import { ready, SANDBOX } from './api/places.js';
22
+ import { clone } from './api/platform.js';
23
+ import { useKept, usePlaces, useRead } from './data.js';
24
+ import { useHost, useTarget } from './host.js';
25
+ import { Publish } from './publish.js';
26
+ import { Attach, compose, Dictate, Files } from './tools.js';
27
+ /** One vocabulary for the mode, on New and in a workspace. */
28
+ export const MODES = [
29
+ { id: 'build', label: 'Build', hint: 'Edits, commits and pushes a branch' },
30
+ { id: 'plan', label: 'Plan', hint: 'Plans the change and writes nothing' },
31
+ ];
32
+ const EFFORTS = [
33
+ { id: 'low', label: 'Low' },
34
+ { id: 'medium', label: 'Medium' },
35
+ { id: 'high', label: 'High' },
36
+ ];
37
+ const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium' };
38
+ const COLUMN = 768;
39
+ export function Landing({ onStarted }) {
40
+ const host = useHost();
41
+ const t = useTarget();
42
+ const signed = Boolean(host.person);
43
+ const [kept, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, FIRST);
44
+ const set = (patch) => keep({ ...kept, ...patch });
45
+ const [draft, setDraft] = useState('');
46
+ const [files, setFiles] = useState([]);
47
+ const [busy, setBusy] = useState(false);
48
+ const [note, setNote] = useState('');
49
+ const [connected, setConnected] = useState(true);
50
+ const [adding, setAdding] = useState(null);
51
+ const places = usePlaces(t, signed);
52
+ const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
53
+ const place = places.value.find((p) => p.id === kept.place) ?? SANDBOX;
54
+ const loadRepos = useCallback(async (q, after) => {
55
+ const page = await repos(t, { q, after: after ?? undefined });
56
+ setConnected(page.connected);
57
+ return { repos: page.repos, next: page.next || null };
58
+ }, [t]);
59
+ const loadBranches = useCallback(async (q, after) => {
60
+ if (!kept.repo)
61
+ return { branches: [], next: null };
62
+ const page = await branches(t, kept.repo.owner, kept.repo.name, { q, after: after ?? undefined });
63
+ return { branches: page.branches.map((b) => ({ name: b.name })), next: page.next || null };
64
+ }, [t, kept.repo]);
65
+ const link = async () => {
66
+ try {
67
+ const to = await connect(t);
68
+ // The console completes the connection at /connectors and brings the
69
+ // person back here: a path, never an origin.
70
+ try {
71
+ window.sessionStorage.setItem('hanzo.return', window.location.pathname + window.location.search);
72
+ }
73
+ catch {
74
+ /* they land on the connectors page and come back themselves */
75
+ }
76
+ window.location.assign(to);
77
+ }
78
+ catch (e) {
79
+ setNote(e instanceof Error ? e.message : 'Could not start the GitHub connection');
80
+ }
81
+ };
82
+ const send = async () => {
83
+ const prompt = draft.trim();
84
+ if (!prompt || busy)
85
+ return;
86
+ if (!signed) {
87
+ host.signIn?.();
88
+ return;
89
+ }
90
+ setBusy(true);
91
+ setNote('');
92
+ try {
93
+ const run = await start(t, {
94
+ prompt: compose(prompt, files),
95
+ repo: kept.repo ? `${kept.repo.owner}/${kept.repo.name}` : undefined,
96
+ base: kept.repo ? kept.branch || kept.repo.default_branch || undefined : undefined,
97
+ targetId: place.id || undefined,
98
+ mode: kept.mode,
99
+ model: kept.model === ENSO ? undefined : kept.model,
100
+ effort: kept.effort,
101
+ });
102
+ setDraft('');
103
+ setFiles([]);
104
+ onStarted(run.session);
105
+ }
106
+ catch (e) {
107
+ setNote(e instanceof Error ? e.message : 'The run could not start');
108
+ }
109
+ finally {
110
+ setBusy(false);
111
+ }
112
+ };
113
+ const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
114
+ id: p.id || 'sandbox',
115
+ label: p.label,
116
+ hint: p.id ? [p.status, p.capacity].filter(Boolean).join(' · ') : 'The platform’s own sandbox',
117
+ disabled: !ready(p),
118
+ place: p,
119
+ })), [places.value]);
120
+ const branch = kept.branch || kept.repo?.default_branch || 'main';
121
+ const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Default', chosen: placeItems.find((i) => i.place.id === place.id) ?? null, items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsx(SizableText, { size: "$1", color: "$soft", children: "A run on a machine uses that machine\u2019s checkout and credentials." }) }), _jsx(RepoSelect, { value: kept.repo, onChange: (r) => set({ repo: r, branch: r.default_branch || 'main' }), load: loadRepos, troubleshoot: { label: 'Troubleshoot GitHub connection', onPress: () => host.open(host.links.github) }, note: "Not all repositories are shown. Type to search.", connect: !connected && !host.admin ? (_jsx(Button, { size: "sm", onPress: () => void link(), children: "Connect GitHub" })) : undefined, action: {
122
+ label: 'Add to project',
123
+ onPress: (r) => setAdding({
124
+ repo: clone(`${r.owner}/${r.name}`),
125
+ title: `${r.owner}/${r.name}`,
126
+ ref: r.default_branch || 'main',
127
+ name: r.name,
128
+ }),
129
+ }, placeholder: "Repository", disabled: !signed }), kept.repo ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
130
+ const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
131
+ return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title: "What\u2019s up next?", mark: _jsx(HanzoMark, { size: 18 }), column: COLUMN }), _jsx(YStack, { flex: 1 }), _jsxs(YStack, { pb: "$2", gap: "$2", children: [note || unhonoured(kept.mode) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) })] }));
132
+ }
package/lib/out.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A link out of the builder: a new tab, no handle back to this page, no
3
+ * referrer. Only ever handed an https address the caller has already checked.
4
+ */
5
+ import type { ReactNode } from 'react';
6
+ export declare function Out({ href, label, children }: {
7
+ href: string;
8
+ label?: string;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
package/lib/out.js ADDED
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Out({ href, label, children }) {
3
+ return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", "aria-label": label, style: { color: 'inherit', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6 }, children: children }));
4
+ }
@@ -0,0 +1,3 @@
1
+ export declare function Project({ slug }: {
2
+ slug: string;
3
+ }): import("react").JSX.Element;