@hanzo/build 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -23
- 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/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +42 -4
- package/lib/api/coding.js +62 -3
- 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/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/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/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +39 -4
- package/lib/api/sessions.js +58 -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 +224 -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 +197 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +50 -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 +125 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +8 -1
- package/lib/desk.js +29 -35
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/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 +8 -4
- package/lib/git.d.ts +9 -0
- package/lib/git.js +74 -0
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +47 -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 +207 -28
- 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/e2e/site.spec.d.ts +0 -1
- package/lib/e2e/site.spec.js +0 -125
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Skills: what the agent knows how to do, each a SKILL.md it reads when an agent
|
|
4
|
+
* names it.
|
|
5
|
+
*
|
|
6
|
+
* Yours is the org's own skills — written here, switched on and off — and the
|
|
7
|
+
* catalogue's skills it has added. Discover is the brand's catalogue; adding one
|
|
8
|
+
* switches it on for the org, which is all adding is: the platform keeps no copy.
|
|
9
|
+
* A skill written here is switched on when it is saved.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { BookOpen } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { Button, Input, Switch, Textarea } from '@hanzo/ui';
|
|
14
|
+
import { Code } from '@hanzo/ui/chat';
|
|
15
|
+
import { useMemo, useState } from 'react';
|
|
16
|
+
import { active, authored, brand, document, nameOf, remove, tool, write } from '../api/skills.js';
|
|
17
|
+
import { toggle } from '../api/tools.js';
|
|
18
|
+
import { useRead } from '../data.js';
|
|
19
|
+
import { useHost, useTarget } from '../host.js';
|
|
20
|
+
import { Add, Confirm, day, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile } from './ui.js';
|
|
21
|
+
const PAGE = 60;
|
|
22
|
+
export function Skills({ view, q, adding, onAdding, onView }) {
|
|
23
|
+
const host = useHost();
|
|
24
|
+
const t = useTarget();
|
|
25
|
+
const signed = Boolean(host.person);
|
|
26
|
+
const own = useRead(signed ? () => authored(t) : null, [], [t, signed]);
|
|
27
|
+
const on = useRead(signed ? () => active(t) : null, [], [t, signed]);
|
|
28
|
+
const shelf = useRead(view === 'discover' ? () => brand(t) : null, null, [t.api, view]);
|
|
29
|
+
const [editing, setEditing] = useState(null);
|
|
30
|
+
const [reading, setReading] = useState(null);
|
|
31
|
+
const [busy, setBusy] = useState('');
|
|
32
|
+
const [note, setNote] = useState('');
|
|
33
|
+
const [shown, setShown] = useState(PAGE);
|
|
34
|
+
const lit = useMemo(() => new Set(on.value.map((x) => x.name)), [on.value]);
|
|
35
|
+
const mine = useMemo(() => new Set(own.value.map((s) => s.name)), [own.value]);
|
|
36
|
+
/** Switch one skill on or off, then read what is on again. */
|
|
37
|
+
const flip = async (name, next) => {
|
|
38
|
+
setBusy(name);
|
|
39
|
+
setNote('');
|
|
40
|
+
try {
|
|
41
|
+
await toggle(t, next ? [tool(name)] : [], next ? [] : [tool(name)]);
|
|
42
|
+
on.reload();
|
|
43
|
+
setNote(next ? `${name} is on` : `${name} is off`);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
setBusy('');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const reader = reading ? (_jsx(Reader, { skill: reading, added: lit.has(tool(reading.name)), signed: signed, busy: busy === reading.name, onFlip: (next) => void flip(reading.name, next), onClose: () => setReading(null) })) : null;
|
|
53
|
+
const editor = adding || editing ? (_jsx(Editor, { skill: editing, onClose: () => {
|
|
54
|
+
setEditing(null);
|
|
55
|
+
onAdding(false);
|
|
56
|
+
}, onSaved: (s, fresh) => {
|
|
57
|
+
setEditing(null);
|
|
58
|
+
onAdding(false);
|
|
59
|
+
own.reload();
|
|
60
|
+
on.reload();
|
|
61
|
+
onView('yours');
|
|
62
|
+
setNote(fresh ? `${s.name} is saved and on` : `${s.name} is saved`);
|
|
63
|
+
}, onDeleted: (name) => {
|
|
64
|
+
setEditing(null);
|
|
65
|
+
own.reload();
|
|
66
|
+
on.reload();
|
|
67
|
+
setNote(`${name} is deleted`);
|
|
68
|
+
} })) : null;
|
|
69
|
+
if (view === 'discover') {
|
|
70
|
+
const all = shelf.value?.skills ?? [];
|
|
71
|
+
const found = all.filter((e) => matches(q, e.name, e.description, e.product));
|
|
72
|
+
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), shelf.error && !shelf.value ? (_jsx(Soft, { children: shelf.error.message })) : !shelf.value ? (_jsx(Soft, { children: "Reading the catalogue\u2026" })) : !found.length ? (_jsx(Soft, { children: all.length ? 'No skill matches.' : 'The catalogue lists no skills.' })) : (_jsxs(_Fragment, { children: [_jsx(SizableText, { size: "$1", color: "$soft", children: found.length === all.length ? `${all.length} skills across ${shelf.value.products.length} products` : `${found.length} of ${all.length} skills` }), _jsx(Grid, { label: "Skills to add", children: found.slice(0, shown).map((e) => (_jsx(Tile, { title: e.name, detail: e.description, meta: e.product, mark: _jsx(Mark, { name: e.name, icon: _jsx(BookOpen, { size: 15 }) }), onOpen: () => setReading(e), action: signed ? _jsx(Add, { name: e.name, added: lit.has(tool(e.name)), busy: busy === e.name, onPress: () => void flip(e.name, true) }) : undefined }, e.name))) }), found.length > shown ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setShown(shown + PAGE), children: "Show more" }) })) : null] })), reader, editor] }));
|
|
73
|
+
}
|
|
74
|
+
if (!signed) {
|
|
75
|
+
return _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your skills." });
|
|
76
|
+
}
|
|
77
|
+
const ownShown = own.value.filter((s) => matches(q, s.name, s.description));
|
|
78
|
+
const added = on.value.filter((x) => nameOf(x.name) && !mine.has(nameOf(x.name)) && matches(q, nameOf(x.name), x.description));
|
|
79
|
+
const error = own.error ?? on.error;
|
|
80
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Line, { children: note }), error && !own.value.length && !on.value.length ? (_jsx(Soft, { children: error.message })) : (own.loading || on.loading) && !own.value.length && !on.value.length ? (_jsx(Soft, { children: "Reading your skills\u2026" })) : !own.value.length && !added.length && !q ? (_jsx(Soft, { action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Discover skills" }), children: "No skills yet. Write one, or add one from the catalogue." })) : (_jsxs(_Fragment, { children: [_jsx(Part, { title: "Written here", detail: "Your organization\u2019s own. Off keeps it, and keeps it out of every agent\u2019s prompt.", children: ownShown.length ? (_jsx(Grid, { label: "Your skills", children: ownShown.map((s) => (_jsx(Tile, { title: s.name, detail: s.description || firstLine(s.content), meta: s.source ? `From ${s.source}` : day(s.created) ? `Saved ${day(s.created)}` : undefined, mark: _jsx(Mark, { name: s.name }), onOpen: () => setEditing(s), action: _jsx(Switch, { checked: lit.has(tool(s.name)), disabled: busy === s.name, onCheckedChange: (v) => void flip(s.name, v), "aria-label": `${s.name} on` }) }, s.id))) })) : (_jsx(SizableText, { size: "$2", color: "$soft", children: own.value.length ? 'None of yours matches.' : 'None yet.' })) }), added.length ? (_jsx(Part, { title: "Added from the catalogue", detail: "On for every agent that names them.", children: _jsx(Grid, { label: "Added skills", children: added.map((x) => {
|
|
81
|
+
const name = nameOf(x.name);
|
|
82
|
+
return (_jsx(Tile, { title: name, detail: x.description, mark: _jsx(Mark, { name: name, icon: _jsx(BookOpen, { size: 15 }) }), onOpen: () => setReading({ name, description: x.description, product: '' }), action: _jsx(Button, { size: "sm", variant: "ghost", disabled: busy === name, onPress: () => void flip(name, false), "aria-label": `Remove ${name}`, children: "Remove" }) }, x.name));
|
|
83
|
+
}) }) })) : null] })), reader, editor] }));
|
|
84
|
+
}
|
|
85
|
+
const firstLine = (md) => md
|
|
86
|
+
.split('\n')
|
|
87
|
+
.map((l) => l.replace(/^#+\s*/, '').trim())
|
|
88
|
+
.find(Boolean) ?? '';
|
|
89
|
+
/** One catalogue skill: what it is, its SKILL.md, and adding or removing it. */
|
|
90
|
+
function Reader({ skill, added, signed, busy, onFlip, onClose, }) {
|
|
91
|
+
const t = useTarget();
|
|
92
|
+
const doc = useRead(() => document(t, skill.name), '', [t.api, skill.name]);
|
|
93
|
+
return (_jsxs(Sheet, { title: skill.name, open: true, onOpenChange: (o) => !o && onClose(), width: 720, children: [skill.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: skill.description })) : null, signed ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(Button, { size: "sm", variant: added ? 'outline' : 'default', disabled: busy, onPress: () => onFlip(!added), children: added ? 'Remove' : 'Add to your skills' }), _jsx(SizableText, { size: "$1", color: "$soft", flex: 1, children: added ? 'On for your organization.' : 'Adding it switches it on for your organization.' })] })) : null, doc.error ? (_jsx(Soft, { children: doc.error.message })) : doc.loading && !doc.value ? (_jsx(Soft, { children: "Reading SKILL.md\u2026" })) : (_jsx(Code, { language: "SKILL.md", value: doc.value, children: _jsx(SizableText, { size: "$1", color: "$ink", style: { ...mono, whiteSpace: 'pre-wrap' }, children: doc.value }) }))] }));
|
|
94
|
+
}
|
|
95
|
+
/** Write a skill, or revise one of the org's own. Its name is its id, so a saved skill keeps its name. */
|
|
96
|
+
function Editor({ skill, onClose, onSaved, onDeleted, }) {
|
|
97
|
+
const t = useTarget();
|
|
98
|
+
const [name, setName] = useState(skill?.name ?? '');
|
|
99
|
+
const [description, setDescription] = useState(skill?.description ?? '');
|
|
100
|
+
const [content, setContent] = useState(skill?.content ?? '');
|
|
101
|
+
const [working, setWorking] = useState(false);
|
|
102
|
+
const [note, setNote] = useState('');
|
|
103
|
+
const [asking, setAsking] = useState(false);
|
|
104
|
+
const save = async () => {
|
|
105
|
+
setWorking(true);
|
|
106
|
+
setNote('');
|
|
107
|
+
try {
|
|
108
|
+
const saved = await write(t, { name: name.trim(), description: description.trim(), content });
|
|
109
|
+
// A new skill is switched on as it is saved; a revised one keeps its switch.
|
|
110
|
+
if (!skill)
|
|
111
|
+
await toggle(t, [tool(saved.name)]);
|
|
112
|
+
onSaved(saved, !skill);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
setNote(e instanceof Error ? e.message : 'The skill was not saved');
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
setWorking(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return (_jsxs(Sheet, { title: skill ? skill.name : 'New skill', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [skill?.source ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["Read from ", skill.source, ". The next push of that repository replaces what is saved here."] })) : null, _jsx(Field, { label: "Name", hint: skill ? 'A skill’s name is its id; save under another name to make a new one.' : 'One lowercase word: letters, digits, _ or -. Agents name it as skill_<name>.', children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "triage", "aria-label": "Name", disabled: Boolean(skill), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The one line an agent reads to decide whether it needs this skill.", children: _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "How we triage an incoming issue", "aria-label": "Description" }) }), _jsx(Field, { label: "SKILL.md", hint: "Markdown: what the skill is for, when to use it, and the steps.", children: _jsx(Textarea, { value: content, onChangeText: setContent, placeholder: '# Triage\n\n1. Read the issue…', "aria-label": "SKILL.md", rows: 14, style: mono }) }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [skill ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save" })] }), skill ? (_jsx(Confirm, { what: skill.name, says: "The skill leaves your organization and every agent that names it.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
122
|
+
await remove(t, skill.id);
|
|
123
|
+
onDeleted(skill.name);
|
|
124
|
+
} })) : null] }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare const mono: {
|
|
3
|
+
fontFamily: string;
|
|
4
|
+
};
|
|
5
|
+
/** A button that holds a card's text reads from the left, not from the middle a button centres it in. */
|
|
6
|
+
export declare const left: {
|
|
7
|
+
readonly textAlign: 'left';
|
|
8
|
+
};
|
|
9
|
+
/** Which half of a tab: what the org has, or what it can add. */
|
|
10
|
+
export type View = 'yours' | 'discover';
|
|
11
|
+
/** What the shell hands a tab: which half, the search, and the Add dialog's state. */
|
|
12
|
+
export interface Pane {
|
|
13
|
+
view: View;
|
|
14
|
+
q: string;
|
|
15
|
+
adding: boolean;
|
|
16
|
+
onAdding: (open: boolean) => void;
|
|
17
|
+
onView: (v: View) => void;
|
|
18
|
+
}
|
|
19
|
+
/** Whether a row answers a search: every word of it somewhere in the row's text. */
|
|
20
|
+
export declare function matches(q: string, ...text: string[]): boolean;
|
|
21
|
+
/** Cards in as many columns as fit, one column on a phone. */
|
|
22
|
+
export declare function Grid({ children, label }: {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
label: string;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
26
|
+
/** A square with the thing's picture, or its initial. */
|
|
27
|
+
export declare function Mark({ name, logo, icon, size }: {
|
|
28
|
+
name: string;
|
|
29
|
+
logo?: string;
|
|
30
|
+
icon?: ReactNode;
|
|
31
|
+
size?: number;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* One card: what it is, a few lines about it, a quiet fact under them, and its
|
|
35
|
+
* action at the top right. Pressing the card opens it; the action is its own
|
|
36
|
+
* control beside that, never inside it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function Tile({ title, detail, meta, mark, action, onOpen, }: {
|
|
39
|
+
title: string;
|
|
40
|
+
detail?: string;
|
|
41
|
+
meta?: string;
|
|
42
|
+
mark?: ReactNode;
|
|
43
|
+
action?: ReactNode;
|
|
44
|
+
onOpen?: () => void;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
|
+
/** The one card the shelf puts first, drawn larger. */
|
|
47
|
+
export declare function Featured({ title, detail, meta, mark, action, onOpen }: {
|
|
48
|
+
title: string;
|
|
49
|
+
detail: string;
|
|
50
|
+
meta?: string;
|
|
51
|
+
mark: ReactNode;
|
|
52
|
+
action?: ReactNode;
|
|
53
|
+
onOpen: () => void;
|
|
54
|
+
}): import("react").JSX.Element;
|
|
55
|
+
/** A card's add control: a plus while it is not yours, a check once it is. */
|
|
56
|
+
export declare function Add({ name, added, busy, onPress }: {
|
|
57
|
+
name: string;
|
|
58
|
+
added: boolean;
|
|
59
|
+
busy?: boolean;
|
|
60
|
+
onPress: () => void;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
62
|
+
/** A dialog with a title, a close control and a body that scrolls inside the window. */
|
|
63
|
+
export declare function Sheet({ title, open, onOpenChange, width, children }: {
|
|
64
|
+
title: string;
|
|
65
|
+
open: boolean;
|
|
66
|
+
onOpenChange: (o: boolean) => void;
|
|
67
|
+
width?: number;
|
|
68
|
+
children: ReactNode;
|
|
69
|
+
}): import("react").JSX.Element;
|
|
70
|
+
/** Asks before a delete; says what goes with it. */
|
|
71
|
+
export declare function Confirm({ what, says, open, onOpenChange, onYes }: {
|
|
72
|
+
what: string;
|
|
73
|
+
says: string;
|
|
74
|
+
open: boolean;
|
|
75
|
+
onOpenChange: (o: boolean) => void;
|
|
76
|
+
onYes: () => Promise<void>;
|
|
77
|
+
}): import("react").JSX.Element;
|
|
78
|
+
/** The quiet line for empty, loading, signed-out and refused. */
|
|
79
|
+
export declare function Soft({ children, action }: {
|
|
80
|
+
children: string;
|
|
81
|
+
action?: ReactNode;
|
|
82
|
+
}): import("react").JSX.Element;
|
|
83
|
+
/** What the last action did, said once, where it was done. */
|
|
84
|
+
export declare function Line({ children }: {
|
|
85
|
+
children: string;
|
|
86
|
+
}): import("react").JSX.Element | null;
|
|
87
|
+
/** A small heading over a run of cards. */
|
|
88
|
+
export declare function Part({ title, detail, children }: {
|
|
89
|
+
title: string;
|
|
90
|
+
detail?: string;
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
}): import("react").JSX.Element;
|
|
93
|
+
/** A labelled control, with a line under it when there is more to say. */
|
|
94
|
+
export declare function Field({ label, hint, children }: {
|
|
95
|
+
label: string;
|
|
96
|
+
hint?: string;
|
|
97
|
+
children: ReactNode;
|
|
98
|
+
}): import("react").JSX.Element;
|
|
99
|
+
/** Two or three choices side by side; the chosen one is filled. */
|
|
100
|
+
export declare function Choice<T extends string>({ value, options, onChange, label }: {
|
|
101
|
+
value: T;
|
|
102
|
+
options: readonly {
|
|
103
|
+
id: T;
|
|
104
|
+
label: string;
|
|
105
|
+
}[];
|
|
106
|
+
onChange: (v: T) => void;
|
|
107
|
+
label: string;
|
|
108
|
+
}): import("react").JSX.Element;
|
|
109
|
+
/** A date from Unix seconds, or ''. */
|
|
110
|
+
export declare const day: (s: number) => string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What every Customize tab is drawn from, so the four read as one page: a grid
|
|
4
|
+
* of cards that stacks on a phone, the card itself with its one action, the
|
|
5
|
+
* featured card, a dialog with a title and a scrolling body, a delete that asks
|
|
6
|
+
* first, and the quiet line for empty, loading, signed-out and refused.
|
|
7
|
+
*/
|
|
8
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { Check, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
10
|
+
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
export const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
13
|
+
/** A button that holds a card's text reads from the left, not from the middle a button centres it in. */
|
|
14
|
+
export const left = { textAlign: 'left' };
|
|
15
|
+
/** Whether a row answers a search: every word of it somewhere in the row's text. */
|
|
16
|
+
export function matches(q, ...text) {
|
|
17
|
+
const words = q.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
18
|
+
if (!words.length)
|
|
19
|
+
return true;
|
|
20
|
+
const hay = text.join(' ').toLowerCase();
|
|
21
|
+
return words.every((w) => hay.includes(w));
|
|
22
|
+
}
|
|
23
|
+
/** Cards in as many columns as fit, one column on a phone. */
|
|
24
|
+
export function Grid({ children, label }) {
|
|
25
|
+
return (_jsx("div", { role: "list", "aria-label": label, style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(100%, 280px), 1fr))', gap: 12 }, children: children }));
|
|
26
|
+
}
|
|
27
|
+
/** A square with the thing's picture, or its initial. */
|
|
28
|
+
export function Mark({ name, logo, icon, size = 32 }) {
|
|
29
|
+
const [gone, setGone] = useState(false);
|
|
30
|
+
return (_jsx(YStack, { width: size, height: size, rounded: "$3", bg: "$raised", items: "center", justify: "center", shrink: 0, overflow: "hidden", "aria-hidden": true, children: logo && !gone ? (_jsx("img", { src: logo, alt: "", width: size, height: size, onError: () => setGone(true), style: { objectFit: 'contain' } })) : ((icon ?? (_jsx(SizableText, { size: "$3", color: "$ink", children: (name.trim().charAt(0) || '?').toUpperCase() })))) }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* One card: what it is, a few lines about it, a quiet fact under them, and its
|
|
34
|
+
* action at the top right. Pressing the card opens it; the action is its own
|
|
35
|
+
* control beside that, never inside it.
|
|
36
|
+
*/
|
|
37
|
+
export function Tile({ title, detail, meta, mark, action, onOpen, }) {
|
|
38
|
+
const body = (_jsxs(_Fragment, { children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: title }), detail ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 3, children: detail })) : null, meta ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, opacity: 0.8, children: meta })) : null] })] }));
|
|
39
|
+
return (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$2", p: "$3", minW: 0, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", hoverStyle: onOpen ? { borderColor: '$edge' } : undefined, children: [onOpen ? (_jsx(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 0, gap: "$3", items: "flex-start", cursor: "pointer", style: left, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: body })) : (_jsx(XStack, { flex: 1, minW: 0, gap: "$3", items: "flex-start", children: body })), action] }));
|
|
40
|
+
}
|
|
41
|
+
/** The one card the shelf puts first, drawn larger. */
|
|
42
|
+
export function Featured({ title, detail, meta, mark, action, onOpen }) {
|
|
43
|
+
return (_jsxs(XStack, { items: "flex-start", gap: "$4", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", rowGap: "$3", children: [_jsxs(XStack, { render: "button", "aria-label": title, onPress: onOpen, flex: 1, minW: 220, gap: "$4", items: "flex-start", cursor: "pointer", style: left, children: [mark, _jsxs(YStack, { flex: 1, minW: 0, gap: "$1.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: "Featured" }), _jsx(SizableText, { size: "$6", color: "$ink", numberOfLines: 1, children: title }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 3, children: detail }), meta ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta })) : null] })] }), action] }));
|
|
44
|
+
}
|
|
45
|
+
/** A card's add control: a plus while it is not yours, a check once it is. */
|
|
46
|
+
export function Add({ name, added, busy, onPress }) {
|
|
47
|
+
if (added) {
|
|
48
|
+
return (_jsx(XStack, { "aria-label": `${name} is added`, width: 32, height: 32, items: "center", justify: "center", shrink: 0, children: _jsx(Check, { size: 16 }) }));
|
|
49
|
+
}
|
|
50
|
+
return (_jsx(Button, { size: "icon-sm", variant: "outline", "aria-label": `Add ${name}`, disabled: busy, onPress: onPress, children: _jsx(Plus, { size: 14 }) }));
|
|
51
|
+
}
|
|
52
|
+
/** A dialog with a title, a close control and a body that scrolls inside the window. */
|
|
53
|
+
export function Sheet({ title, open, onOpenChange, width = 560, children }) {
|
|
54
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: width, showCloseButton: false, maxH: "88vh", gap: "$3", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { numberOfLines: 1, flex: 1, children: title }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsx(YStack, { gap: "$3", overflow: "scroll", minH: 0, shrink: 1, children: children })] }) }));
|
|
55
|
+
}
|
|
56
|
+
/** Asks before a delete; says what goes with it. */
|
|
57
|
+
export function Confirm({ what, says, open, onOpenChange, onYes }) {
|
|
58
|
+
const [busy, setBusy] = useState(false);
|
|
59
|
+
const [note, setNote] = useState('');
|
|
60
|
+
const yes = async () => {
|
|
61
|
+
setBusy(true);
|
|
62
|
+
setNote('');
|
|
63
|
+
try {
|
|
64
|
+
await onYes();
|
|
65
|
+
onOpenChange(false);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setBusy(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return (_jsxs(Sheet, { title: `Delete ${what}?`, open: open, onOpenChange: onOpenChange, width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: says }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", variant: "destructive", disabled: busy, onPress: () => void yes(), children: "Delete" })] })] }));
|
|
75
|
+
}
|
|
76
|
+
/** The quiet line for empty, loading, signed-out and refused. */
|
|
77
|
+
export function Soft({ children, action }) {
|
|
78
|
+
return (_jsxs(YStack, { py: "$6", items: "center", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }), action] }));
|
|
79
|
+
}
|
|
80
|
+
/** What the last action did, said once, where it was done. */
|
|
81
|
+
export function Line({ children }) {
|
|
82
|
+
if (!children)
|
|
83
|
+
return null;
|
|
84
|
+
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: children }));
|
|
85
|
+
}
|
|
86
|
+
/** A small heading over a run of cards. */
|
|
87
|
+
export function Part({ title, detail, children }) {
|
|
88
|
+
return (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), detail ? (_jsx(SizableText, { size: "$1", color: "$soft", children: detail })) : null] }), children] }));
|
|
89
|
+
}
|
|
90
|
+
/** A labelled control, with a line under it when there is more to say. */
|
|
91
|
+
export function Field({ label, hint, children }) {
|
|
92
|
+
return (_jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: label }), children, hint ? (_jsx(SizableText, { size: "$1", color: "$soft", children: hint })) : null] }));
|
|
93
|
+
}
|
|
94
|
+
/** Two or three choices side by side; the chosen one is filled. */
|
|
95
|
+
export function Choice({ value, options, onChange, label }) {
|
|
96
|
+
return (_jsx(XStack, { role: "group", "aria-label": label, p: 2, gap: 2, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", self: "flex-start", children: options.map((o) => (_jsx(XStack, { render: "button", "aria-pressed": o.id === value, onPress: () => onChange(o.id), px: "$3", py: "$1", rounded: "$2", bg: o.id === value ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: o.id === value ? '$ink' : '$soft', children: o.label }) }, o.id))) }));
|
|
97
|
+
}
|
|
98
|
+
/** A date from Unix seconds, or ''. */
|
|
99
|
+
export const day = (s) => (s ? new Date(s * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : '');
|
package/lib/desk.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type DropdownMenuProps } from '@hanzo/ui';
|
|
1
2
|
import type { Event } from './api/sessions.ts';
|
|
2
|
-
export declare function Desk({ id, repo, branch, base, environment, mode, pr, events, live, refused, retry, onRetry, onHide, }: {
|
|
3
|
+
export declare function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu, refused, retry, onRetry, onHide, }: {
|
|
3
4
|
id: string;
|
|
4
5
|
repo: string;
|
|
5
6
|
branch: string;
|
|
@@ -10,8 +11,14 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ev
|
|
|
10
11
|
href: string;
|
|
11
12
|
label: string;
|
|
12
13
|
};
|
|
14
|
+
title: string;
|
|
15
|
+
project: string;
|
|
16
|
+
/** The sandbox the run holds, or '' before it leases one and on a machine. */
|
|
17
|
+
sandbox: string;
|
|
13
18
|
events: Event[];
|
|
14
19
|
live: boolean;
|
|
20
|
+
/** What can be done to the run — rename, share, copy its id — as its header's menu offers it. */
|
|
21
|
+
menu?: NonNullable<DropdownMenuProps['items']>;
|
|
15
22
|
refused: string;
|
|
16
23
|
/** The refusal's one action: Retry, or Sign in when nobody is. */
|
|
17
24
|
retry?: string;
|
package/lib/desk.js
CHANGED
|
@@ -1,36 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The pane beside a run's transcript:
|
|
4
|
-
*
|
|
3
|
+
* The pane beside a run's transcript: its codebase's environment, what it
|
|
4
|
+
* pushed, its sandbox's desktop and shell, its files, and what it listens to.
|
|
5
5
|
*/
|
|
6
6
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
7
|
-
import {
|
|
7
|
+
import { Info, MoreHorizontal, PanelRightClose } from '@hanzogui/lucide-icons-2';
|
|
8
8
|
import { Button, DropdownMenu } from '@hanzo/ui';
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
-
import { blob, tree } from './api/git.js';
|
|
11
10
|
import { shell } from './api/turn.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
import { Door } from './door.js';
|
|
12
|
+
import { Environment } from './environment.js';
|
|
13
|
+
import { Files } from './files.js';
|
|
14
|
+
import { Git } from './git.js';
|
|
15
|
+
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
16
|
+
const BLEED = ['desktop', 'terminal'];
|
|
17
|
+
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu = [], refused, retry = 'Retry', onRetry, onHide, }) {
|
|
18
|
+
// A setup run's work IS the environment, so it opens there.
|
|
19
|
+
const [tab, setTab] = useState(mode === 'setup' ? 'environment' : 'terminal');
|
|
18
20
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
19
|
-
const ref = branch || base || 'main';
|
|
20
21
|
const lines = shell(events);
|
|
21
|
-
const
|
|
22
|
-
|
|
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, retry: retry, 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] })] }));
|
|
22
|
+
const bleed = BLEED.includes(tab);
|
|
23
|
+
return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", display: "none", $md: { display: 'flex' }, children: [_jsxs(XStack, { px: "$2", pt: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "subscriptions", tab: tab, onPick: setTab, children: "Subscriptions" }), _jsx(XStack, { flex: 1 }), onHide ? (_jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) })) : null, _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, live: live, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'files' ? (_jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: live, mode: mode, project: project, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'subscriptions' ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", px: "$6", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 320 }, children: "Ask an agent to subscribe to a Slack channel or thread, a pull request, or a timer." }) })) : null] })] }));
|
|
34
24
|
}
|
|
35
25
|
function TabButton({ id, tab, onPick, children }) {
|
|
36
26
|
const on = tab === id;
|
|
@@ -40,17 +30,21 @@ function Fact({ label, value }) {
|
|
|
40
30
|
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
31
|
}
|
|
42
32
|
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)', whiteSpace: 'pre' };
|
|
43
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The Terminal tab: a shell in the run's sandbox, in the same working tree as the
|
|
35
|
+
* agent, while the run holds one; and the commands the agent itself ran, which
|
|
36
|
+
* outlive the sandbox. The shell is a tmux session named for the run, so a
|
|
37
|
+
* reopened tab reattaches to it.
|
|
38
|
+
*/
|
|
39
|
+
function Terminal({ id, sandbox, lines, live, refused, retry, onRetry, }) {
|
|
40
|
+
const reachable = live && Boolean(sandbox);
|
|
41
|
+
// The shell while there is one, unless the log was asked for.
|
|
42
|
+
const [log, setLog] = useState(false);
|
|
43
|
+
const showShell = reachable && !log;
|
|
44
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [reachable ? (_jsx(XStack, { render: "button", "aria-label": "Shell", onPress: () => setLog(false), px: "$2", py: "$1", rounded: "$2", bg: showShell ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$1", color: showShell ? '$ink' : '$soft', children: "Shell" }) })) : null, _jsx(XStack, { render: "button", "aria-label": "Agent log", onPress: () => setLog(true), px: "$2", py: "$1", rounded: "$2", bg: showShell ? 'transparent' : '$hover', children: _jsx(SizableText, { size: "$1", color: showShell ? '$soft' : '$ink', children: "Agent log" }) })] }), showShell ? _jsx(Door, { which: "terminal", sandbox: sandbox, live: live, session: `run-${id.replace(/^sess_/, '').slice(0, 12)}` }) : _jsx(Log, { lines: lines, live: live, refused: refused, retry: retry, onRetry: onRetry })] }));
|
|
45
|
+
}
|
|
46
|
+
function Log({ lines, live, refused, retry, onRetry, }) {
|
|
44
47
|
const prompt = 'workspace';
|
|
45
48
|
const idle = lines.length === 0 && !live;
|
|
46
|
-
return (_jsxs(YStack, { flex: 1, minH: 0,
|
|
47
|
-
}
|
|
48
|
-
function Files({ repo, refName }) {
|
|
49
|
-
const t = useTarget();
|
|
50
|
-
const [dir, setDir] = useState('');
|
|
51
|
-
const [file, setFile] = useState('');
|
|
52
|
-
const list = useRead(repo ? () => tree(t, repo, refName, dir) : null, [], [t, repo, refName, dir]);
|
|
53
|
-
const body = useRead(file ? () => blob(t, repo, refName, file) : null, null, [t, repo, refName, file]);
|
|
54
|
-
const parent = dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '';
|
|
55
|
-
return (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", children: [dir ? (_jsx(XStack, { render: "button", "aria-label": "Up one directory", onPress: () => { setDir(parent); setFile(''); }, px: "$1", py: "$1", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Up" }) })) : null, _jsxs(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [repo, refName ? `@${refName}` : '', dir ? `/${dir}` : ''] })] }), list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the branch\u2026" })) : !repo ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "This run has no repository yet." })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "This directory is empty." })) : (list.value.map((e) => (_jsxs(XStack, { render: "button", "aria-label": e.dir ? `Open ${e.name}` : `Read ${e.name}`, onPress: () => (e.dir ? (setDir(e.path), setFile('')) : setFile(e.path)), items: "center", gap: "$2", py: "$1", hoverStyle: { bg: '$hover' }, children: [e.dir ? _jsx(Folder, { size: 14 }) : _jsx(File, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: e.name })] }, e.path)))), file && body.value ? (_jsxs(YStack, { gap: "$1", pt: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: file }), _jsx(SizableText, { size: "$1", color: "$ink", style: { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: body.value.binary ? 'Binary file' : body.value.truncated ? 'Too large to show' : body.value.text || 'Empty file' })] })) : null, file && body.error ? (_jsx(SizableText, { size: "$1", color: "$soft", children: body.error.message })) : null] }));
|
|
49
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", py: "$3", gap: "$1", children: [idle ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", py: "$8", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: refused || 'This run finished without writing a command.' }), refused ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onRetry, children: retry })) : null] })) : (lines.map((line, i) => line.role === 'cmd' ? (_jsxs(XStack, { gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(SizableText, { flex: 1, size: "$1", color: "$ink", style: mono, children: line.text })] }, i)) : (_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: line.text }, i)))), live ? (_jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: `${prompt} $` }), _jsx(YStack, { width: 7, height: 14, bg: "$ink" })] })) : null] }), _jsx(XStack, { px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "Commands appear here as the run writes them." }) })] }));
|
|
56
50
|
}
|
package/lib/door.d.ts
ADDED
package/lib/door.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One door into a run's sandbox — its screen or its shell — framed from cloud's
|
|
4
|
+
* own page with a fresh ticket in its address.
|
|
5
|
+
*
|
|
6
|
+
* The page says when its socket is up. A page that says it failed, or silence
|
|
7
|
+
* past the deadline, is a failed connection, and Retry mints a new ticket: a
|
|
8
|
+
* ticket is spent the moment its socket opens, so reloading the old address can
|
|
9
|
+
* only fail again.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, YStack } from '@hanzo/gui';
|
|
12
|
+
import { Button } from '@hanzo/ui';
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { door } from './api/sandbox.js';
|
|
15
|
+
import { useTarget } from './host.js';
|
|
16
|
+
/** What each page signs its messages to the window that frames it with. */
|
|
17
|
+
const SOURCE = { screen: 'hanzo-screen', terminal: 'hanzo-term' };
|
|
18
|
+
const WHAT = { screen: 'desktop', terminal: 'terminal' };
|
|
19
|
+
const DEADLINE = 20_000;
|
|
20
|
+
export function Door({ which, sandbox, live, session }) {
|
|
21
|
+
const t = useTarget();
|
|
22
|
+
const frame = useRef(null);
|
|
23
|
+
const [src, setSrc] = useState('');
|
|
24
|
+
const [state, setState] = useState('opening');
|
|
25
|
+
const [why, setWhy] = useState('');
|
|
26
|
+
const [attempt, setAttempt] = useState(0);
|
|
27
|
+
const up = live && Boolean(sandbox);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!up)
|
|
30
|
+
return;
|
|
31
|
+
let gone = false;
|
|
32
|
+
setSrc('');
|
|
33
|
+
setState('opening');
|
|
34
|
+
setWhy('');
|
|
35
|
+
door(t, sandbox, which, session).then((u) => {
|
|
36
|
+
if (!gone)
|
|
37
|
+
setSrc(u);
|
|
38
|
+
}, (e) => {
|
|
39
|
+
if (gone)
|
|
40
|
+
return;
|
|
41
|
+
setState('failed');
|
|
42
|
+
setWhy(e instanceof Error ? e.message : '');
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
gone = true;
|
|
46
|
+
};
|
|
47
|
+
}, [t, sandbox, which, session, up, attempt]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!src)
|
|
50
|
+
return;
|
|
51
|
+
const origin = new URL(src, window.location.href).origin;
|
|
52
|
+
const hear = (e) => {
|
|
53
|
+
if (e.origin !== origin || e.source !== frame.current?.contentWindow)
|
|
54
|
+
return;
|
|
55
|
+
const m = e.data;
|
|
56
|
+
if (!m || m.source !== SOURCE[which])
|
|
57
|
+
return;
|
|
58
|
+
if (m.retry === true) {
|
|
59
|
+
setAttempt((a) => a + 1);
|
|
60
|
+
}
|
|
61
|
+
else if (m.ready === true) {
|
|
62
|
+
setState('open');
|
|
63
|
+
}
|
|
64
|
+
else if (m.ready === false) {
|
|
65
|
+
setState('failed');
|
|
66
|
+
setWhy(typeof m.why === 'string' ? m.why : '');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener('message', hear);
|
|
70
|
+
const late = setTimeout(() => setState((s) => (s === 'opening' ? 'failed' : s)), DEADLINE);
|
|
71
|
+
return () => {
|
|
72
|
+
window.removeEventListener('message', hear);
|
|
73
|
+
clearTimeout(late);
|
|
74
|
+
};
|
|
75
|
+
}, [src, which]);
|
|
76
|
+
if (!live) {
|
|
77
|
+
return _jsx(Shut, { title: "Not running", body: `This run’s ${WHAT[which]} closed when the run stopped.` });
|
|
78
|
+
}
|
|
79
|
+
if (!sandbox) {
|
|
80
|
+
return _jsx(Shut, { title: "Starting", body: `The ${WHAT[which]} opens once the run’s sandbox is up.` });
|
|
81
|
+
}
|
|
82
|
+
if (state === 'failed') {
|
|
83
|
+
return (_jsx(Shut, { title: "Failed to connect", body: why || `Unable to reach the ${WHAT[which]}. The run may have stopped.`, children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => setAttempt((a) => a + 1), children: "Retry" }) }));
|
|
84
|
+
}
|
|
85
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", bg: "#000", children: [src ? (_jsx("iframe", { ref: frame, src: src, title: which === 'screen' ? 'The run’s desktop' : 'The run’s terminal', allow: "clipboard-read; clipboard-write", style: { border: 0, width: '100%', height: '100%', display: 'block', background: '#000' } })) : null, state === 'opening' ? (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", pointerEvents: "none", children: _jsx(SizableText, { size: "$2", color: "$soft", children: `Connecting to the ${WHAT[which]}…` }) })) : null] }));
|
|
86
|
+
}
|
|
87
|
+
function Shut({ title, body, children }) {
|
|
88
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", px: "$6", py: "$8", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center', maxWidth: 360 }, children: body }), children] }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function Environment({ repo, busy }: {
|
|
2
|
+
repo: string;
|
|
3
|
+
busy: boolean;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
/**
|
|
6
|
+
* New's offer to set a codebase up. An agent onboards it, or an org admin saves
|
|
7
|
+
* it empty and writes the scripts by hand beside the next run.
|
|
8
|
+
*/
|
|
9
|
+
export declare function SetupDialog({ repo, open, onOpenChange, onStart, onSaved, }: {
|
|
10
|
+
repo: string;
|
|
11
|
+
open: boolean;
|
|
12
|
+
onOpenChange: (o: boolean) => void;
|
|
13
|
+
onStart: () => Promise<void>;
|
|
14
|
+
onSaved: () => void;
|
|
15
|
+
}): import("react").JSX.Element;
|