@hanzo/build 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -25
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/coding.d.ts +31 -1
- package/lib/api/coding.js +50 -1
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sessions.d.ts +32 -4
- package/lib/api/sessions.js +42 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +221 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +199 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +52 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +127 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +4 -1
- package/lib/desk.js +3 -16
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +4 -4
- package/lib/git.d.ts +4 -0
- package/lib/git.js +6 -2
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +30 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +184 -32
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
package/lib/prefs.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The person's own settings, read once for the whole builder and shared by
|
|
4
|
+
* everything that follows them: Settings writes them, New starts from the
|
|
5
|
+
* coding defaults and says the name, dictation listens in the chosen language,
|
|
6
|
+
* and the page takes the theme, the text size and the motion.
|
|
7
|
+
*
|
|
8
|
+
* The last document read is kept in this browser, per org, so the page is
|
|
9
|
+
* drawn the person's way before the platform answers. A save shows at once and
|
|
10
|
+
* is taken back, key by key, if the platform refuses it.
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
import { merge, read, save } from './api/pref.js';
|
|
14
|
+
import { useKept, useRead } from './data.js';
|
|
15
|
+
import { useHost, useTarget } from './host.js';
|
|
16
|
+
const Context = createContext(null);
|
|
17
|
+
export function PrefsProvider({ children }) {
|
|
18
|
+
const host = useHost();
|
|
19
|
+
const t = useTarget();
|
|
20
|
+
const signed = Boolean(host.person);
|
|
21
|
+
const [kept, keep] = useKept(`hanzo.build.pref.${host.org ?? 'none'}`, {});
|
|
22
|
+
const now = useRef(kept);
|
|
23
|
+
const doc = useRead(signed ? () => read(t) : null, null, [t, signed]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!doc.value)
|
|
26
|
+
return;
|
|
27
|
+
now.current = doc.value;
|
|
28
|
+
keep(doc.value);
|
|
29
|
+
}, [doc.value, keep]);
|
|
30
|
+
const write = useCallback(async (patch) => {
|
|
31
|
+
if (!signed)
|
|
32
|
+
throw new Error('Sign in to save your settings.');
|
|
33
|
+
const before = now.current;
|
|
34
|
+
now.current = merge(before, patch);
|
|
35
|
+
keep(now.current);
|
|
36
|
+
try {
|
|
37
|
+
now.current = await save(t, patch);
|
|
38
|
+
keep(now.current);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// Only the keys this change named go back; another change since stands.
|
|
42
|
+
const undo = {};
|
|
43
|
+
for (const k of Object.keys(patch))
|
|
44
|
+
undo[k] = before[k] ?? null;
|
|
45
|
+
now.current = merge(now.current, undo);
|
|
46
|
+
keep(now.current);
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}, [signed, t, keep]);
|
|
50
|
+
const value = useMemo(() => ({ prefs: kept, loading: doc.loading, error: doc.error, save: write }), [kept, doc.loading, doc.error, write]);
|
|
51
|
+
return (_jsxs(Context.Provider, { value: value, children: [_jsx(Look, { prefs: kept }), children] }));
|
|
52
|
+
}
|
|
53
|
+
export function usePrefs() {
|
|
54
|
+
const p = useContext(Context);
|
|
55
|
+
if (!p)
|
|
56
|
+
throw new Error('The builder was rendered without its settings');
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
/** The type scale each text size is drawn at. Medium is the design system's own. */
|
|
60
|
+
export const SCALE = { small: 0.9, medium: 1, large: 1.15 };
|
|
61
|
+
/** Every animation and transition finished at once, the way the system setting does it. */
|
|
62
|
+
const STILL = '*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}';
|
|
63
|
+
/**
|
|
64
|
+
* The saved theme handed to the host, and the text size and motion put on the
|
|
65
|
+
* page. A stylesheet rather than a property on <html>: the design system's own
|
|
66
|
+
* root re-applies its appearance on mount, and a rule marked important outlasts
|
|
67
|
+
* that, while the builder is on the page and no longer.
|
|
68
|
+
*/
|
|
69
|
+
function Look({ prefs }) {
|
|
70
|
+
const host = useHost();
|
|
71
|
+
const { theme, text, motion } = prefs;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (theme && theme !== host.theme)
|
|
74
|
+
host.chooseTheme?.(theme);
|
|
75
|
+
// The host's theme is followed, not watched: a choice made elsewhere stands until this one changes.
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [theme]);
|
|
78
|
+
const rules = [
|
|
79
|
+
text && text !== 'medium' ? `html:root{--type-scale:${SCALE[text]}!important}` : '',
|
|
80
|
+
motion === 'reduced' ? STILL : '',
|
|
81
|
+
].join('');
|
|
82
|
+
return rules ? _jsx("style", { "data-hanzo-build": "look", children: rules }) : null;
|
|
83
|
+
}
|
package/lib/prose.d.ts
ADDED
package/lib/prose.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Markdown an agent writes, drawn as text (markdown.ts reads it).
|
|
4
|
+
*
|
|
5
|
+
* @hanzo/ui leaves the markdown pipeline to the surface, and the builder needs
|
|
6
|
+
* only the part agents use. Nothing here produces HTML from the text: every
|
|
7
|
+
* piece is a React element holding a string, and a link is drawn only for an
|
|
8
|
+
* http(s) address, opened in a new tab.
|
|
9
|
+
*/
|
|
10
|
+
import { ScrollView, SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
+
import { Code } from '@hanzo/ui/chat';
|
|
12
|
+
import { blocks, spans } from './markdown.js';
|
|
13
|
+
import { Out } from './out.js';
|
|
14
|
+
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
15
|
+
function Inline({ text }) {
|
|
16
|
+
return (_jsx(_Fragment, { children: spans(text).map((s, i) => {
|
|
17
|
+
switch (s.kind) {
|
|
18
|
+
case 'code':
|
|
19
|
+
return (_jsx(SizableText, { size: "$2", color: "$ink", bg: "$hover", px: "$1", rounded: "$1", style: mono, children: s.text }, i));
|
|
20
|
+
case 'strong':
|
|
21
|
+
return (_jsx(SizableText, { size: "$3", color: "$ink", fontWeight: "600", children: s.text }, i));
|
|
22
|
+
case 'em':
|
|
23
|
+
return (_jsx(SizableText, { size: "$3", color: "$ink", fontStyle: "italic", children: s.text }, i));
|
|
24
|
+
case 'link':
|
|
25
|
+
return (_jsx(Out, { href: s.href, children: _jsx(SizableText, { size: "$3", color: "$ink", textDecorationLine: "underline", children: s.text }) }, i));
|
|
26
|
+
default:
|
|
27
|
+
return s.text;
|
|
28
|
+
}
|
|
29
|
+
}) }));
|
|
30
|
+
}
|
|
31
|
+
const SIZES = ['$6', '$5', '$4', '$4', '$3', '$3'];
|
|
32
|
+
function Draw({ block }) {
|
|
33
|
+
switch (block.kind) {
|
|
34
|
+
case 'heading':
|
|
35
|
+
return (_jsx(SizableText, { size: SIZES[block.level - 1] ?? '$3', color: "$ink", fontWeight: "600", pt: "$1", children: _jsx(Inline, { text: block.text }) }));
|
|
36
|
+
case 'code':
|
|
37
|
+
return (_jsx(Code, { language: block.lang || 'text', value: block.text, children: block.text }));
|
|
38
|
+
case 'rule':
|
|
39
|
+
return _jsx(YStack, { height: 1, bg: "$borderColor", my: "$1" });
|
|
40
|
+
case 'quote':
|
|
41
|
+
return (_jsx(YStack, { borderLeftWidth: 2, borderColor: "$borderColor", pl: "$3", children: _jsx(SizableText, { size: "$3", color: "$soft", children: _jsx(Inline, { text: block.text }) }) }));
|
|
42
|
+
case 'list':
|
|
43
|
+
return (_jsx(YStack, { gap: "$1", role: "list", children: block.items.map((item, i) => (_jsxs(XStack, { gap: "$2", pl: item.depth * 16, role: "listitem", children: [_jsx(SizableText, { size: "$3", color: "$soft", minW: 16, style: { textAlign: 'right' }, children: block.ordered ? `${block.start + i}.` : '•' }), _jsx(SizableText, { flex: 1, minW: 0, size: "$3", color: "$ink", children: _jsx(Inline, { text: item.text }) })] }, i))) }));
|
|
44
|
+
case 'table':
|
|
45
|
+
return (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, children: _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", minW: "100%", children: [block.head, ...block.rows].map((row, r) => (_jsx(XStack, { borderTopWidth: r ? 1 : 0, borderColor: "$borderColor", bg: r ? 'transparent' : '$hover', children: block.head.map((_, c) => (_jsx(SizableText, { flex: 1, minW: 96, px: "$2", py: "$1.5", size: "$2", color: "$ink", fontWeight: r ? '400' : '600', children: _jsx(Inline, { text: row[c] ?? '' }) }, c))) }, r))) }) }));
|
|
46
|
+
default:
|
|
47
|
+
return (_jsx(SizableText, { size: "$3", color: "$ink", children: _jsx(Inline, { text: block.text }) }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** A markdown text, drawn. */
|
|
51
|
+
export function Prose({ text }) {
|
|
52
|
+
return (_jsx(YStack, { gap: "$2.5", minW: 0, children: blocks(text).map((b, i) => (_jsx(Draw, { block: b }, i))) }));
|
|
53
|
+
}
|
package/lib/route.d.ts
CHANGED
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
* '' the empty state: a new run
|
|
5
5
|
* sess_<32 hex> one run
|
|
6
6
|
* -/<screen> a builder screen
|
|
7
|
+
* -/settings[/<section>] a section of Settings
|
|
8
|
+
* -/customize[/<tab>] skills, connectors and plugins: what the agent brings to a run
|
|
9
|
+
* -/plans the plans an organization can be on
|
|
7
10
|
* <slug> a project's workspace
|
|
8
11
|
*
|
|
9
12
|
* Unambiguous by construction: a project slug is `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`,
|
|
10
13
|
* so it can hold neither the `_` a session id carries nor start with `-`.
|
|
11
14
|
* Anything else is not an address here and reads as the empty state.
|
|
12
15
|
*/
|
|
13
|
-
export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp';
|
|
16
|
+
export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp' | 'plans';
|
|
17
|
+
/** Settings, one section each. Every setting the builder has lives on this page, none elsewhere. */
|
|
18
|
+
export declare const SECTIONS: readonly ['general', 'account', 'privacy', 'billing', 'usage', 'capabilities', 'memory', 'code', 'environments', 'machines', 'keys', 'members', 'integrations', 'notifications'];
|
|
19
|
+
export type Section = (typeof SECTIONS)[number];
|
|
20
|
+
/** Customize, one tab each: what the agent brings to a run, yours and to discover. */
|
|
21
|
+
export declare const TABS: readonly ['skills', 'connectors', 'plugins', 'agents'];
|
|
22
|
+
export type Tab = (typeof TABS)[number];
|
|
14
23
|
export type Route = {
|
|
15
24
|
kind: 'new';
|
|
16
25
|
} | {
|
|
@@ -19,6 +28,12 @@ export type Route = {
|
|
|
19
28
|
} | {
|
|
20
29
|
kind: 'screen';
|
|
21
30
|
screen: Screen;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'settings';
|
|
33
|
+
section: Section;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'customize';
|
|
36
|
+
tab: Tab;
|
|
22
37
|
} | {
|
|
23
38
|
kind: 'project';
|
|
24
39
|
slug: string;
|
package/lib/route.js
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
1
|
+
/** Settings, one section each. Every setting the builder has lives on this page, none elsewhere. */
|
|
2
|
+
export const SECTIONS = [
|
|
3
|
+
'general',
|
|
4
|
+
'account',
|
|
5
|
+
'privacy',
|
|
6
|
+
'billing',
|
|
7
|
+
'usage',
|
|
8
|
+
'capabilities',
|
|
9
|
+
'memory',
|
|
10
|
+
'code',
|
|
11
|
+
'environments',
|
|
12
|
+
'machines',
|
|
13
|
+
'keys',
|
|
14
|
+
'members',
|
|
15
|
+
'integrations',
|
|
16
|
+
'notifications',
|
|
17
|
+
];
|
|
18
|
+
/** Customize, one tab each: what the agent brings to a run, yours and to discover. */
|
|
19
|
+
export const TABS = ['skills', 'connectors', 'plugins', 'agents'];
|
|
1
20
|
export const SESSION = /^sess_[0-9a-f]{32}$/;
|
|
2
21
|
export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
3
|
-
const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync', 'mcp'];
|
|
22
|
+
const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync', 'mcp', 'plans'];
|
|
4
23
|
export function route(path) {
|
|
5
24
|
const p = path.replace(/^\/+|\/+$/g, '');
|
|
6
25
|
if (SESSION.test(p))
|
|
7
26
|
return { kind: 'run', id: p };
|
|
27
|
+
if (p === '-/settings' || p.startsWith('-/settings/')) {
|
|
28
|
+
const section = (p.slice('-/settings/'.length) || 'general');
|
|
29
|
+
return SECTIONS.includes(section) ? { kind: 'settings', section } : { kind: 'new' };
|
|
30
|
+
}
|
|
31
|
+
if (p === '-/customize' || p.startsWith('-/customize/')) {
|
|
32
|
+
const tab = (p.slice('-/customize/'.length) || 'skills');
|
|
33
|
+
return TABS.includes(tab) ? { kind: 'customize', tab } : { kind: 'new' };
|
|
34
|
+
}
|
|
8
35
|
if (p.startsWith('-/')) {
|
|
9
36
|
const s = p.slice(2);
|
|
10
37
|
return SCREENS.includes(s) ? { kind: 'screen', screen: s } : { kind: 'new' };
|
|
@@ -22,6 +49,10 @@ export function path(r) {
|
|
|
22
49
|
return r.id;
|
|
23
50
|
case 'screen':
|
|
24
51
|
return `-/${r.screen}`;
|
|
52
|
+
case 'settings':
|
|
53
|
+
return r.section === 'general' ? '-/settings' : `-/settings/${r.section}`;
|
|
54
|
+
case 'customize':
|
|
55
|
+
return r.tab === 'skills' ? '-/customize' : `-/customize/${r.tab}`;
|
|
25
56
|
case 'project':
|
|
26
57
|
return r.slug;
|
|
27
58
|
}
|
package/lib/run.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* One run, live: its transcript as it streams, the
|
|
4
|
-
*
|
|
3
|
+
* One run, live: its transcript as it streams, the controls that reach it —
|
|
4
|
+
* steer, pause, resume, stop — the pull request once it pushes one, and the
|
|
5
|
+
* follow-up that continues it once it has finished.
|
|
5
6
|
*
|
|
6
7
|
* The detail read is the record and the stream adds to it; the two overlap
|
|
7
8
|
* whenever a turn lands between the read and the subscribe, so turns are merged
|
|
@@ -10,19 +11,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
11
|
*
|
|
11
12
|
* Steering is a queue the run drains between steps: a press is confirmed as
|
|
12
13
|
* RECORDED, and the status is left to the feed to correct.
|
|
14
|
+
*
|
|
15
|
+
* A sandbox run is one agent invocation that takes its task on its command line
|
|
16
|
+
* (apps/coding steer.go), so what continues it is a new run: a paused run is
|
|
17
|
+
* carried on, and a finished one followed up, by `POST /v1/agent/coding` from
|
|
18
|
+
* the branch its work was kept on — the platform's own continue, done here.
|
|
13
19
|
*/
|
|
14
20
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
15
|
-
import { ExternalLink, GitPullRequest, PanelRight } from '@hanzogui/lucide-icons-2';
|
|
16
|
-
import { Button } from '@hanzo/ui';
|
|
17
|
-
import {
|
|
21
|
+
import { Copy, ExternalLink, GitPullRequest, Globe, MoreHorizontal, PanelRight, Pencil } from '@hanzogui/lucide-icons-2';
|
|
22
|
+
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
23
|
+
import { Steer } from '@hanzo/ui/agents';
|
|
18
24
|
import { Composer } from '@hanzo/ui/chat';
|
|
19
25
|
import { useEffect, useMemo, useState } from 'react';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
26
|
+
import { approve, followUp, start } from './api/coding.js';
|
|
27
|
+
import { list, message, pause, publish, rename, resume, stop, story, took } from './api/sessions.js';
|
|
28
|
+
import { answer, cards, outcome, pull, settled, steps } from './api/turn.js';
|
|
22
29
|
import { useKept, useRead, useRun } from './data.js';
|
|
23
30
|
import { Desk } from './desk.js';
|
|
24
31
|
import { useHost, useTarget } from './host.js';
|
|
25
32
|
import { Out } from './out.js';
|
|
33
|
+
import { Transcript } from './transcript.js';
|
|
26
34
|
const LIVE = new Set(['running', 'paused', '']);
|
|
27
35
|
export function Run({ id }) {
|
|
28
36
|
const host = useHost();
|
|
@@ -35,21 +43,49 @@ export function Run({ id }) {
|
|
|
35
43
|
const [busy, setBusy] = useState(false);
|
|
36
44
|
const [notify, setNotify] = useState(false);
|
|
37
45
|
const [desk, setDesk] = useKept('hanzo.build.desk', true);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
// Stop was pressed here: the run keeps its work after it answers, and says so with its status.
|
|
47
|
+
const [stopping, setStopping] = useState(false);
|
|
48
|
+
const [naming, setNaming] = useState(false);
|
|
49
|
+
const [name, setName] = useState('');
|
|
50
|
+
const [copied, setCopied] = useState('');
|
|
51
|
+
const mode = record?.mode ?? '';
|
|
52
|
+
const shown = useMemo(() => cards(events, mode), [events, mode]);
|
|
41
53
|
const end = outcome(events);
|
|
42
54
|
const plan = useMemo(() => steps(events), [events]);
|
|
55
|
+
const kept = useMemo(() => settled(events), [events]);
|
|
43
56
|
const state = status || end.status;
|
|
44
57
|
const pr = pull(record?.pr ?? '', record?.repo ?? '');
|
|
45
|
-
const
|
|
46
|
-
const
|
|
58
|
+
const live = LIVE.has(state);
|
|
59
|
+
const running = signed && live && !detail.error;
|
|
60
|
+
const paused = running && state === 'paused';
|
|
61
|
+
const finished = signed && !live && !detail.error && Boolean(record);
|
|
62
|
+
// A sandbox run is held by the coding service, which pauses it with its work
|
|
63
|
+
// kept and has no resume of its own; a machine drains its commands itself.
|
|
64
|
+
const held = (record?.environment || 'sandbox') === 'sandbox';
|
|
65
|
+
// A follow-up clones what the run kept, so it waits until the run has said where that is.
|
|
66
|
+
const waiting = (paused && held && !kept.settled) || (finished && stopping && !kept.settled);
|
|
67
|
+
const title = record?.title || detail.value?.title || (detail.loading ? '' : 'Untitled run');
|
|
47
68
|
// A setup run explores and installs before it answers, which takes minutes.
|
|
48
|
-
const setup =
|
|
69
|
+
const setup = mode === 'setup';
|
|
49
70
|
const [seen, setSeen] = useKept('hanzo.build.setup.seen', false);
|
|
50
71
|
// How long setup runs have taken here: the ones this person can see, finished.
|
|
51
72
|
const past = useRead(setup && running ? () => list(t, { kind: 'coding', status: 'done', limit: 500 }) : null, [], [t, setup, running]);
|
|
52
73
|
const span = took(past.value, 'setup');
|
|
74
|
+
const earlier = record
|
|
75
|
+
? {
|
|
76
|
+
id,
|
|
77
|
+
title,
|
|
78
|
+
repo: record.repo,
|
|
79
|
+
base: record.base,
|
|
80
|
+
environment: record.environment,
|
|
81
|
+
project: record.project,
|
|
82
|
+
mode,
|
|
83
|
+
pushed: kept.pushed,
|
|
84
|
+
}
|
|
85
|
+
: null;
|
|
86
|
+
const planned = mode === 'plan' && finished && state === 'done' ? answer(events) : '';
|
|
87
|
+
// What the recorded read left out: it carries the latest fifty turns.
|
|
88
|
+
const hidden = Math.max(0, (detail.value?.events ?? 0) - (detail.value?.recent.length ?? 0));
|
|
53
89
|
useEffect(() => {
|
|
54
90
|
if (!notify || running)
|
|
55
91
|
return;
|
|
@@ -72,34 +108,139 @@ export function Run({ id }) {
|
|
|
72
108
|
setNotify(true);
|
|
73
109
|
setNote('');
|
|
74
110
|
};
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
/** Something that answers a sentence for the note, or throws the reason. */
|
|
112
|
+
const act = async (work, failed) => {
|
|
113
|
+
if (busy)
|
|
77
114
|
return;
|
|
78
115
|
setBusy(true);
|
|
79
116
|
setNote('');
|
|
80
117
|
try {
|
|
81
|
-
await
|
|
82
|
-
setDraft('');
|
|
83
|
-
setNote('Sent — the run reads it before its next step');
|
|
118
|
+
setNote(await work());
|
|
84
119
|
}
|
|
85
120
|
catch (e) {
|
|
86
|
-
setNote(e instanceof Error ? e.message :
|
|
121
|
+
setNote(e instanceof Error ? e.message : failed);
|
|
87
122
|
}
|
|
88
123
|
finally {
|
|
89
124
|
setBusy(false);
|
|
90
125
|
}
|
|
91
126
|
};
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
/** A new run from this one's work, opened. A paused run it carries on is let go once the new one is admitted. */
|
|
128
|
+
const carry = async (said, how = 'follow') => {
|
|
129
|
+
if (!earlier)
|
|
130
|
+
throw new Error('This run is still being read');
|
|
131
|
+
const run = await start(t, how === 'approve' ? approve(earlier, said) : followUp(earlier, said));
|
|
132
|
+
if (paused)
|
|
133
|
+
await stop(t, id, 'Continued in a follow-up run').catch(() => undefined);
|
|
134
|
+
setDraft('');
|
|
135
|
+
host.go(run.session);
|
|
136
|
+
return '';
|
|
137
|
+
};
|
|
138
|
+
const send = () => {
|
|
139
|
+
const words = draft.trim();
|
|
140
|
+
if (!words)
|
|
141
|
+
return;
|
|
142
|
+
if (running && !(paused && held)) {
|
|
143
|
+
void act(async () => {
|
|
144
|
+
await message(t, id, words);
|
|
145
|
+
setDraft('');
|
|
146
|
+
return 'Sent — recorded on this run';
|
|
147
|
+
}, 'Could not reach this run');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
void act(() => carry(words), 'The follow-up could not start');
|
|
151
|
+
};
|
|
152
|
+
const command = (c) => {
|
|
153
|
+
switch (c) {
|
|
154
|
+
case 'stop':
|
|
155
|
+
void act(async () => {
|
|
156
|
+
await stop(t, id);
|
|
157
|
+
setStopping(true);
|
|
158
|
+
return 'Stop requested — the run keeps its work on its branch';
|
|
159
|
+
}, 'Could not stop this run');
|
|
160
|
+
return;
|
|
161
|
+
case 'pause':
|
|
162
|
+
void act(async () => {
|
|
163
|
+
await pause(t, id);
|
|
164
|
+
return held ? 'Pause requested — the run keeps its work on its branch and waits' : 'Pause asked — the machine pauses when it reads it';
|
|
165
|
+
}, 'Could not pause this run');
|
|
166
|
+
return;
|
|
167
|
+
case 'resume':
|
|
168
|
+
if (held)
|
|
169
|
+
void act(() => carry(''), 'The run could not go on');
|
|
170
|
+
else
|
|
171
|
+
void act(async () => {
|
|
172
|
+
await resume(t, id);
|
|
173
|
+
return 'Resume asked — the machine goes on when it reads it';
|
|
174
|
+
}, 'Could not resume this run');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const copy = async (what, value) => {
|
|
94
179
|
try {
|
|
95
|
-
await
|
|
96
|
-
|
|
180
|
+
await navigator.clipboard.writeText(value);
|
|
181
|
+
setCopied(what);
|
|
97
182
|
}
|
|
98
|
-
catch
|
|
99
|
-
setNote(
|
|
183
|
+
catch {
|
|
184
|
+
setNote('This browser would not copy.');
|
|
100
185
|
}
|
|
101
186
|
};
|
|
102
|
-
|
|
187
|
+
const share = (on) => act(async () => {
|
|
188
|
+
await publish(t, id, on);
|
|
189
|
+
detail.reload();
|
|
190
|
+
return on ? 'Shared — anyone with the link can read this run’s story' : 'No longer shared';
|
|
191
|
+
}, 'Could not change who can read this run');
|
|
192
|
+
const link = record ? story(t, record.org || host.org || '', record.project) : '';
|
|
193
|
+
const menu = [
|
|
194
|
+
...(signed && record
|
|
195
|
+
? [
|
|
196
|
+
{
|
|
197
|
+
key: 'rename',
|
|
198
|
+
label: 'Rename',
|
|
199
|
+
icon: _jsx(Pencil, { size: 16 }),
|
|
200
|
+
onSelect: () => {
|
|
201
|
+
setName(title);
|
|
202
|
+
setNaming(true);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
]
|
|
206
|
+
: []),
|
|
207
|
+
// Sharing opens the public build route, which is addressed by the project a run built.
|
|
208
|
+
...(signed && record?.project
|
|
209
|
+
? [
|
|
210
|
+
record.published
|
|
211
|
+
? { key: 'unshare', label: 'Stop sharing', icon: _jsx(Globe, { size: 16 }), onSelect: () => void share(false) }
|
|
212
|
+
: { key: 'share', label: 'Share publicly', icon: _jsx(Globe, { size: 16 }), description: 'Anyone with the link can read it', onSelect: () => void share(true) },
|
|
213
|
+
]
|
|
214
|
+
: []),
|
|
215
|
+
...(record?.published && link
|
|
216
|
+
? [{ key: 'link', label: copied === 'link' ? 'Copied' : 'Copy public link', icon: _jsx(Copy, { size: 16 }), description: link, onSelect: () => void copy('link', link) }]
|
|
217
|
+
: []),
|
|
218
|
+
{ key: 'copy', label: copied === 'id' ? 'Copied' : 'Copy run id', icon: _jsx(Copy, { size: 16 }), description: id, onSelect: () => void copy('id', id) },
|
|
219
|
+
];
|
|
220
|
+
const save = () => void act(async () => {
|
|
221
|
+
await rename(t, id, name);
|
|
222
|
+
setNaming(false);
|
|
223
|
+
detail.reload();
|
|
224
|
+
return 'Renamed';
|
|
225
|
+
}, 'Could not rename this run');
|
|
226
|
+
const placeholder = !signed
|
|
227
|
+
? 'Sign in to follow up'
|
|
228
|
+
: waiting
|
|
229
|
+
? paused
|
|
230
|
+
? 'Pausing — the run is keeping its work…'
|
|
231
|
+
: 'Stopping — the run is keeping its work…'
|
|
232
|
+
: paused && held
|
|
233
|
+
? 'Continue this run with a follow up'
|
|
234
|
+
: running
|
|
235
|
+
? setup
|
|
236
|
+
? 'Add a follow up for the setup agent'
|
|
237
|
+
: 'Add a follow up'
|
|
238
|
+
: finished
|
|
239
|
+
? 'Follow up — continues in a new run'
|
|
240
|
+
: 'Reading this run…';
|
|
241
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, menu.length ? (_jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Manage this run", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: menu })) : null] }), _jsx(Transcript, { cards: shown,
|
|
242
|
+
// A paused run's agent was stopped where it stood; what it was running is cut off.
|
|
243
|
+
live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve'), 'The build could not start') : undefined, approving: busy, empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
|
|
103
244
|
? detail.error.message
|
|
104
245
|
: detail.loading
|
|
105
246
|
? 'Reading this run…'
|
|
@@ -108,13 +249,24 @@ export function Run({ id }) {
|
|
|
108
249
|
? 'Setting up environment'
|
|
109
250
|
: 'Starting…'
|
|
110
251
|
: 'Waiting for the run’s first step…' })) : (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to follow this run." }), _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" })] })) }), _jsxs(YStack, { pb: "$4", pt: "$2", gap: "$2", children: [end.problem ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The branch is pushed, and the pull request could not be opened." })) : null, note || (signed && refused) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || refused })) : null, plan.length ? (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Steps" }), _jsx(SizableText, { size: "$1", color: "$soft", children: String(plan.length) })] }), plan.map((s, i) => {
|
|
111
|
-
|
|
252
|
+
// Only a run still working is on a step.
|
|
253
|
+
const current = running && !s.done && plan.findIndex((x) => !x.done) === i;
|
|
112
254
|
return (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: s.done || current ? '$ink' : '$soft', children: s.done ? '✓' : current ? '●' : '○' }), _jsx(SizableText, { size: "$2", color: s.done ? '$soft' : '$ink', numberOfLines: 1, children: s.name })] }, s.name));
|
|
113
|
-
})] })) : null, setup && running && !seen ? _jsx(Onboarding, { onDone: () => setSeen(true) }) : null, running ? (_jsxs(XStack, { items: "center",
|
|
114
|
-
?
|
|
115
|
-
?
|
|
116
|
-
: '
|
|
117
|
-
:
|
|
255
|
+
})] })) : null, setup && running && !seen ? _jsx(Onboarding, { onDone: () => setSeen(true) }) : null, running ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 160, children: paused
|
|
256
|
+
? kept.pushed
|
|
257
|
+
? 'Paused — its work so far is on its branch.'
|
|
258
|
+
: 'Paused.'
|
|
259
|
+
: setup
|
|
260
|
+
? span
|
|
261
|
+
? `Environment setup takes ~${span[0] === span[1] ? span[0] : `${span[0]}–${span[1]}`} minutes.`
|
|
262
|
+
: 'Environment setup takes several minutes.'
|
|
263
|
+
: 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
|
|
264
|
+
...(paused ? ['pause'] : ['resume']),
|
|
265
|
+
...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
|
|
266
|
+
] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: running && !(paused && held) ? 'Steer this run' : 'Follow up on this run' })] })] }), desk ? (_jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', events: events, live: running, menu: menu, refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
|
|
267
|
+
if ((e.key ?? e.nativeEvent?.key) === 'Enter')
|
|
268
|
+
save();
|
|
269
|
+
} }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
|
|
118
270
|
}
|
|
119
271
|
/** What a setup run does, said once per browser the first time one is watched. */
|
|
120
272
|
function Onboarding({ onDone }) {
|
package/lib/section.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* host's own `go`.
|
|
12
12
|
*/
|
|
13
13
|
import { SizableText } from '@hanzo/gui';
|
|
14
|
-
import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate,
|
|
14
|
+
import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, SlidersHorizontal, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
15
15
|
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
17
|
import { pinBoard } from './choice.js';
|
|
@@ -48,5 +48,5 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
|
48
48
|
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === 'codebases', onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), children: "Codebase" }), _jsx(SidebarItem, { icon: _jsx(Kanban, { size: 16, "aria-hidden": true }), active: screen === 'projects', onPress: () => go(path({ kind: 'screen', screen: 'projects' })), children: "Projects" }), _jsx(SidebarItem, { icon: _jsx(CircleDot, { size: 16, "aria-hidden": true }), active: screen === 'issues', onPress: () => {
|
|
49
49
|
pinBoard(host.org, '');
|
|
50
50
|
go(path({ kind: 'screen', screen: 'issues' }));
|
|
51
|
-
}, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(
|
|
51
|
+
}, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(SlidersHorizontal, { size: 16, "aria-hidden": true }), active: r.kind === 'customize', onPress: () => go(path({ kind: 'customize', tab: 'connectors' })), children: "Customize" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
|
|
52
52
|
}
|