@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Plugins: TypeScript connectors the org builds for the runtime to run.
|
|
4
|
+
*
|
|
5
|
+
* Yours is what the org built, each with the source that runs. Building is the
|
|
6
|
+
* gate — the platform bundles and compiles the source and keeps it only if both
|
|
7
|
+
* succeed, and a failure says why in the bundler's words. Describe an API
|
|
8
|
+
* instead of writing the TypeScript and a model writes it; the result opens to
|
|
9
|
+
* be read. A plugin carries no credential: it names the connector whose
|
|
10
|
+
* credential it reads when it runs. Discover lists what this deployment mounts,
|
|
11
|
+
* which is read-only.
|
|
12
|
+
*/
|
|
13
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
|
+
import { Puzzle } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { Button, Input, Textarea } from '@hanzo/ui';
|
|
16
|
+
import { Code } from '@hanzo/ui/chat';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
import { authored, build, mounted, remove } from '../api/plugins.js';
|
|
19
|
+
import { useRead } from '../data.js';
|
|
20
|
+
import { useHost, useTarget } from '../host.js';
|
|
21
|
+
import { Choice, Confirm, day, Field, Grid, Line, Mark, matches, mono, Sheet, Soft, Tile } from './ui.js';
|
|
22
|
+
const kb = (n) => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
|
|
23
|
+
export function Plugins({ view, q, adding, onAdding, onView }) {
|
|
24
|
+
const host = useHost();
|
|
25
|
+
const t = useTarget();
|
|
26
|
+
const signed = Boolean(host.person);
|
|
27
|
+
const mine = useRead(signed && view === 'yours' ? () => authored(t) : null, [], [t, signed, view]);
|
|
28
|
+
const mounts = useRead(signed && view === 'discover' ? () => mounted(t) : null, [], [t, signed, view]);
|
|
29
|
+
const [opened, setOpened] = useState(null);
|
|
30
|
+
const [note, setNote] = useState('');
|
|
31
|
+
const sheets = (_jsxs(_Fragment, { children: [adding ? (_jsx(Build, { onClose: () => onAdding(false), onBuilt: (p, said) => {
|
|
32
|
+
onAdding(false);
|
|
33
|
+
onView('yours');
|
|
34
|
+
mine.reload();
|
|
35
|
+
setNote(said);
|
|
36
|
+
setOpened(p);
|
|
37
|
+
} })) : null, opened ? (_jsx(Source, { plugin: opened, onClose: () => setOpened(null), onDeleted: () => {
|
|
38
|
+
setNote(`${opened.name} is deleted`);
|
|
39
|
+
setOpened(null);
|
|
40
|
+
mine.reload();
|
|
41
|
+
} })) : null] }));
|
|
42
|
+
if (!signed) {
|
|
43
|
+
return _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: view === 'discover' ? 'Sign in to see what this deployment mounts.' : 'Sign in to see your plugins.' });
|
|
44
|
+
}
|
|
45
|
+
if (view === 'discover') {
|
|
46
|
+
const shown = mounts.value.filter((m) => matches(q, m.name, ...m.prefixes));
|
|
47
|
+
return (_jsxs(YStack, { gap: "$3", children: [mounts.error && !mounts.value.length ? (_jsx(Soft, { children: mounts.error.message })) : mounts.loading && !mounts.value.length ? (_jsx(Soft, { children: "Reading what is mounted\u2026" })) : !shown.length ? (_jsx(Soft, { children: mounts.value.length ? 'Nothing mounted matches.' : 'This deployment reports nothing mounted.' })) : (_jsxs(_Fragment, { children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " subsystems mounted in this deployment. They are part of the platform, so there is nothing to add."] }), _jsx(Grid, { label: "Mounted subsystems", children: shown.map((m) => (_jsx(Tile, { title: m.name, detail: m.prefixes.join(' '), mark: _jsx(Mark, { name: m.name, icon: _jsx(Puzzle, { size: 15 }) }) }, m.name))) })] })), sheets] }));
|
|
48
|
+
}
|
|
49
|
+
const shown = mine.value.filter((p) => matches(q, p.name, p.provider));
|
|
50
|
+
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Soft, { children: mine.error.message })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Reading your plugins\u2026" })) : !mine.value.length ? (_jsx(Soft, { action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children: "Build a plugin" }), children: "No plugins yet. Write a connector in TypeScript, or describe an API and have one written." })) : !shown.length ? (_jsx(Soft, { children: "No plugin matches." })) : (_jsx(Grid, { label: "Your plugins", children: shown.map((p) => (_jsx(Tile, { title: p.name, detail: p.provider ? `Reads the ${p.provider} connector’s credential when it runs.` : 'Needs no credential.', meta: day(p.built) ? `Built ${day(p.built)}` : undefined, mark: _jsx(Mark, { name: p.name }), onOpen: () => setOpened(p) }, p.id))) })), sheets] }));
|
|
51
|
+
}
|
|
52
|
+
const WAYS = [
|
|
53
|
+
{ id: 'source', label: 'TypeScript' },
|
|
54
|
+
{ id: 'spec', label: 'Describe an API' },
|
|
55
|
+
];
|
|
56
|
+
/** Build a plugin from its TypeScript, or from a description of the API it calls. */
|
|
57
|
+
function Build({ onClose, onBuilt }) {
|
|
58
|
+
const t = useTarget();
|
|
59
|
+
const [name, setName] = useState('');
|
|
60
|
+
const [provider, setProvider] = useState('');
|
|
61
|
+
const [way, setWay] = useState('source');
|
|
62
|
+
// Each way keeps its own text, so switching between them loses neither.
|
|
63
|
+
const [source, setSource] = useState('');
|
|
64
|
+
const [spec, setSpec] = useState('');
|
|
65
|
+
const [working, setWorking] = useState(false);
|
|
66
|
+
const [note, setNote] = useState('');
|
|
67
|
+
const go = async () => {
|
|
68
|
+
setWorking(true);
|
|
69
|
+
setNote(way === 'spec' ? 'Writing and building it…' : 'Building it…');
|
|
70
|
+
try {
|
|
71
|
+
const out = await build(t, { name: name.trim(), provider, source: way === 'source' ? source : '', spec: way === 'spec' ? spec : '' });
|
|
72
|
+
onBuilt(out.plugin, `${out.plugin.name} built: ${kb(out.bytes)}${out.generated ? ', written from your description — read it below' : ''}.`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
setNote(e instanceof Error ? e.message : 'The build failed');
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
setWorking(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -. The runtime loads it by this name.", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "acme", "aria-label": "Name", autoCapitalize: "none" }) }), _jsx(Field, { label: "Connector", hint: "Optional. The connector whose credential the plugin reads when it runs. A plugin never holds a key itself.", children: _jsx(Input, { value: provider, onChangeText: setProvider, placeholder: "acme", "aria-label": "Connector", autoCapitalize: "none" }) }), _jsx(Choice, { label: "Build from", value: way, options: WAYS, onChange: setWay }), _jsx(Field, { label: way === 'source' ? 'Source' : 'The API', hint: way === 'source'
|
|
82
|
+
? 'It is bundled and compiled in the runtime that will run it, and kept only if both succeed.'
|
|
83
|
+
: 'An OpenAPI document, or prose naming the endpoints. A model writes the TypeScript; you read it before it runs.', children: _jsx(Textarea, { value: way === 'source' ? source : spec, onChangeText: way === 'source' ? setSource : setSpec, "aria-label": way === 'source' ? 'Source' : 'The API', placeholder: way === 'source' ? 'export const acme = createProvider({ … })' : 'POST /v1/things creates a thing…', rows: 14, style: mono }) }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: "Build" })] })] }));
|
|
84
|
+
}
|
|
85
|
+
/** A built plugin's source, as the runtime will run it, and deleting it. */
|
|
86
|
+
function Source({ plugin, onClose, onDeleted }) {
|
|
87
|
+
const t = useTarget();
|
|
88
|
+
const [asking, setAsking] = useState(false);
|
|
89
|
+
return (_jsxs(Sheet, { title: plugin.name, open: true, onOpenChange: (o) => !o && onClose(), width: 760, children: [_jsxs(SizableText, { size: "$2", color: "$soft", children: [plugin.provider ? `Reads the ${plugin.provider} connector’s credential when it runs.` : 'Needs no credential.', day(plugin.built) ? ` Built ${day(plugin.built)}.` : ''] }), _jsx(Code, { language: "TypeScript", value: plugin.source, children: _jsx(SizableText, { size: "$1", color: "$ink", style: { ...mono, whiteSpace: 'pre' }, children: plugin.source }) }), _jsx(YStack, { items: "flex-start", children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete plugin" }) }), _jsx(Confirm, { what: plugin.name, says: "The runtime can no longer load it.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
90
|
+
await remove(t, plugin.id);
|
|
91
|
+
onDeleted();
|
|
92
|
+
} })] }));
|
|
93
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
// What every run in the org loads is an org admin's to change; a member reads it.
|
|
27
|
+
const may = signed && host.admin;
|
|
28
|
+
const own = useRead(signed ? () => authored(t) : null, [], [t, signed]);
|
|
29
|
+
const on = useRead(signed ? () => active(t) : null, [], [t, signed]);
|
|
30
|
+
const shelf = useRead(view === 'discover' ? () => brand(t) : null, null, [t.api, view]);
|
|
31
|
+
const [editing, setEditing] = useState(null);
|
|
32
|
+
const [reading, setReading] = useState(null);
|
|
33
|
+
const [busy, setBusy] = useState('');
|
|
34
|
+
const [note, setNote] = useState('');
|
|
35
|
+
const [shown, setShown] = useState(PAGE);
|
|
36
|
+
const lit = useMemo(() => new Set(on.value.map((x) => x.name)), [on.value]);
|
|
37
|
+
const mine = useMemo(() => new Set(own.value.map((s) => s.name)), [own.value]);
|
|
38
|
+
/** Switch one skill on or off, then read what is on again. */
|
|
39
|
+
const flip = async (name, next) => {
|
|
40
|
+
setBusy(name);
|
|
41
|
+
setNote('');
|
|
42
|
+
try {
|
|
43
|
+
await toggle(t, next ? [tool(name)] : [], next ? [] : [tool(name)]);
|
|
44
|
+
on.reload();
|
|
45
|
+
setNote(next ? `${name} is on` : `${name} is off`);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
setBusy('');
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const reader = reading ? (_jsx(Reader, { skill: reading, added: lit.has(tool(reading.name)), signed: may, busy: busy === reading.name, onFlip: (next) => void flip(reading.name, next), onClose: () => setReading(null) })) : null;
|
|
55
|
+
const editor = adding || editing ? (_jsx(Editor, { skill: editing, may: may, onClose: () => {
|
|
56
|
+
setEditing(null);
|
|
57
|
+
onAdding(false);
|
|
58
|
+
}, onSaved: (s, fresh) => {
|
|
59
|
+
setEditing(null);
|
|
60
|
+
onAdding(false);
|
|
61
|
+
own.reload();
|
|
62
|
+
on.reload();
|
|
63
|
+
onView('yours');
|
|
64
|
+
setNote(fresh ? `${s.name} is saved and on` : `${s.name} is saved`);
|
|
65
|
+
}, onDeleted: (name) => {
|
|
66
|
+
setEditing(null);
|
|
67
|
+
own.reload();
|
|
68
|
+
on.reload();
|
|
69
|
+
setNote(`${name} is deleted`);
|
|
70
|
+
} })) : null;
|
|
71
|
+
if (view === 'discover') {
|
|
72
|
+
const all = shelf.value?.skills ?? [];
|
|
73
|
+
const found = all.filter((e) => matches(q, e.name, e.description, e.product));
|
|
74
|
+
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: may ? _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] }));
|
|
75
|
+
}
|
|
76
|
+
if (!signed) {
|
|
77
|
+
return _jsx(Soft, { action: host.signIn ? _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) : undefined, children: "Sign in to see your skills." });
|
|
78
|
+
}
|
|
79
|
+
const ownShown = own.value.filter((s) => matches(q, s.name, s.description));
|
|
80
|
+
const added = on.value.filter((x) => nameOf(x.name) && !mine.has(nameOf(x.name)) && matches(q, nameOf(x.name), x.description));
|
|
81
|
+
const error = own.error ?? on.error;
|
|
82
|
+
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: may ? (_jsx(Switch, { checked: lit.has(tool(s.name)), disabled: busy === s.name, onCheckedChange: (v) => void flip(s.name, v), "aria-label": `${s.name} on` })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: lit.has(tool(s.name)) ? 'On' : 'Off' })) }, 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) => {
|
|
83
|
+
const name = nameOf(x.name);
|
|
84
|
+
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: may ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: busy === name, onPress: () => void flip(name, false), "aria-label": `Remove ${name}`, children: "Remove" })) : undefined }, x.name));
|
|
85
|
+
}) }) })) : null] })), reader, editor] }));
|
|
86
|
+
}
|
|
87
|
+
const firstLine = (md) => md
|
|
88
|
+
.split('\n')
|
|
89
|
+
.map((l) => l.replace(/^#+\s*/, '').trim())
|
|
90
|
+
.find(Boolean) ?? '';
|
|
91
|
+
/** One catalogue skill: what it is, its SKILL.md, and adding or removing it. */
|
|
92
|
+
function Reader({ skill, added, signed, busy, onFlip, onClose, }) {
|
|
93
|
+
const t = useTarget();
|
|
94
|
+
const doc = useRead(() => document(t, skill.name), '', [t.api, skill.name]);
|
|
95
|
+
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 }) }))] }));
|
|
96
|
+
}
|
|
97
|
+
/** Write a skill, or revise one of the org's own. Its name is its id, so a saved skill keeps its name. */
|
|
98
|
+
function Editor({ skill, may, onClose, onSaved, onDeleted, }) {
|
|
99
|
+
const t = useTarget();
|
|
100
|
+
const [name, setName] = useState(skill?.name ?? '');
|
|
101
|
+
const [description, setDescription] = useState(skill?.description ?? '');
|
|
102
|
+
const [content, setContent] = useState(skill?.content ?? '');
|
|
103
|
+
const [working, setWorking] = useState(false);
|
|
104
|
+
const [note, setNote] = useState('');
|
|
105
|
+
const [asking, setAsking] = useState(false);
|
|
106
|
+
const save = async () => {
|
|
107
|
+
setWorking(true);
|
|
108
|
+
setNote('');
|
|
109
|
+
try {
|
|
110
|
+
const saved = await write(t, { name: name.trim(), description: description.trim(), content });
|
|
111
|
+
// A new skill is switched on as it is saved; a revised one keeps its switch.
|
|
112
|
+
if (!skill)
|
|
113
|
+
await toggle(t, [tool(saved.name)]);
|
|
114
|
+
onSaved(saved, !skill);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
setNote(e instanceof Error ? e.message : 'The skill was not saved');
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
setWorking(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
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 || (may ? '' : 'An org admin writes and deletes the organization’s skills.') }), _jsxs(XStack, { gap: "$2", items: "center", children: [skill && may ? (_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: may ? 'Cancel' : 'Close' }), may ? (_jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save" })) : null] }), skill ? (_jsx(Confirm, { what: skill.name, says: "The skill leaves your organization and every agent that names it.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
124
|
+
await remove(t, skill.id);
|
|
125
|
+
onDeleted(skill.name);
|
|
126
|
+
} })) : null] }));
|
|
127
|
+
}
|
|
@@ -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, title, project, sandbox, 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;
|
|
@@ -16,6 +17,8 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ti
|
|
|
16
17
|
sandbox: string;
|
|
17
18
|
events: Event[];
|
|
18
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']>;
|
|
19
22
|
refused: string;
|
|
20
23
|
/** The refusal's one action: Retry, or Sign in when nobody is. */
|
|
21
24
|
retry?: string;
|
package/lib/desk.js
CHANGED
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
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
10
|
import { shell } from './api/turn.js';
|
|
@@ -14,26 +14,13 @@ import { Files } from './files.js';
|
|
|
14
14
|
import { Git } from './git.js';
|
|
15
15
|
/** The tabs that are a surface of their own, drawn edge to edge. */
|
|
16
16
|
const BLEED = ['desktop', 'terminal'];
|
|
17
|
-
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, refused, retry = 'Retry', onRetry, onHide, }) {
|
|
17
|
+
export function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, menu = [], refused, retry = 'Retry', onRetry, onHide, }) {
|
|
18
18
|
// A setup run's work IS the environment, so it opens there.
|
|
19
19
|
const [tab, setTab] = useState(mode === 'setup' ? 'environment' : 'terminal');
|
|
20
|
-
const [copied, setCopied] = useState(false);
|
|
21
20
|
const name = repo.split('/').filter(Boolean).pop() || repo;
|
|
22
21
|
const lines = shell(events);
|
|
23
22
|
const bleed = BLEED.includes(tab);
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
await navigator.clipboard.writeText(id);
|
|
27
|
-
setCopied(true);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
setCopied(false);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
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: [
|
|
34
|
-
{ key: 'copy', label: copied ? 'Copied' : 'Copy run id', icon: _jsx(Copy, { size: 16 }), description: id, onSelect: () => void copy() },
|
|
35
|
-
{ key: 'details', label: 'Details', onSelect: () => setTab('environment') },
|
|
36
|
-
] })] }), _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] })] }));
|
|
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] })] }));
|
|
37
24
|
}
|
|
38
25
|
function TabButton({ id, tab, onPick, children }) {
|
|
39
26
|
const on = tab === id;
|
package/lib/find.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RailSession } from '@hanzo/ui/chat';
|
|
2
|
+
export declare function Find({ open, onOpenChange, recents, onOpen, }: {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (o: boolean) => void;
|
|
5
|
+
recents: RailSession[];
|
|
6
|
+
onOpen: (id: string) => void;
|
|
7
|
+
}): import("react").JSX.Element;
|
package/lib/find.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Finding a run: the rail's search icon. The org's coding runs as the platform
|
|
4
|
+
* lists them, newest first, filtered by status and paged back as far as they
|
|
5
|
+
* go; what is typed narrows the runs read so far by name and codebase. Until
|
|
6
|
+
* the first page lands it searches the rail's own recents.
|
|
7
|
+
*/
|
|
8
|
+
import { ScrollView, SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { page } from './api/sessions.js';
|
|
12
|
+
import { useHost, useTarget } from './host.js';
|
|
13
|
+
/** The statuses the platform filters on, and every run. */
|
|
14
|
+
const FILTERS = [
|
|
15
|
+
{ id: '', label: 'All' },
|
|
16
|
+
{ id: 'running', label: 'Running' },
|
|
17
|
+
{ id: 'paused', label: 'Paused' },
|
|
18
|
+
{ id: 'done', label: 'Done' },
|
|
19
|
+
{ id: 'error', label: 'Error' },
|
|
20
|
+
];
|
|
21
|
+
const PAGE = 50;
|
|
22
|
+
export function Find({ open, onOpenChange, recents, onOpen, }) {
|
|
23
|
+
const host = useHost();
|
|
24
|
+
const t = useTarget();
|
|
25
|
+
const signed = Boolean(host.person);
|
|
26
|
+
const [q, setQ] = useState('');
|
|
27
|
+
const [status, setStatus] = useState('');
|
|
28
|
+
const [rows, setRows] = useState(null);
|
|
29
|
+
const [next, setNext] = useState('');
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState('');
|
|
32
|
+
// A page asked for under another status or an earlier opening lands on nothing.
|
|
33
|
+
const asked = useRef(0);
|
|
34
|
+
const read = (after) => {
|
|
35
|
+
const mine = after ? asked.current : ++asked.current;
|
|
36
|
+
setLoading(true);
|
|
37
|
+
setError('');
|
|
38
|
+
return page(t, { kind: 'coding', status, limit: PAGE, after })
|
|
39
|
+
.then((p) => {
|
|
40
|
+
if (mine !== asked.current)
|
|
41
|
+
return;
|
|
42
|
+
setRows((prev) => (after && prev ? [...prev, ...p.sessions.filter((s) => !prev.some((r) => r.id === s.id))] : p.sessions));
|
|
43
|
+
setNext(p.next);
|
|
44
|
+
})
|
|
45
|
+
.catch((e) => {
|
|
46
|
+
if (mine === asked.current)
|
|
47
|
+
setError(e instanceof Error ? e.message : 'The runs could not be read');
|
|
48
|
+
})
|
|
49
|
+
.finally(() => {
|
|
50
|
+
if (mine === asked.current)
|
|
51
|
+
setLoading(false);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
// Opening, or another status, reads from the newest again.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!open || !signed)
|
|
57
|
+
return;
|
|
58
|
+
setRows(null);
|
|
59
|
+
setNext('');
|
|
60
|
+
void read('');
|
|
61
|
+
// `read` closes over the status and target this effect is keyed on.
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [open, signed, status, t]);
|
|
64
|
+
const hits = useMemo(() => {
|
|
65
|
+
const n = q.trim().toLowerCase();
|
|
66
|
+
const all = rows
|
|
67
|
+
? rows.map((s) => ({ id: s.id, title: s.title || 'Untitled run', line: [s.repo, s.status].filter(Boolean).join(' · ') }))
|
|
68
|
+
: status
|
|
69
|
+
? []
|
|
70
|
+
: recents.map((r) => ({ id: r.id, title: r.title, line: '' }));
|
|
71
|
+
return n ? all.filter((h) => `${h.title} ${h.line}`.toLowerCase().includes(n)) : all;
|
|
72
|
+
}, [q, rows, recents, status]);
|
|
73
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, children: [_jsx(DialogTitle, { children: "Find a run" }), _jsx(Input, { autoFocus: true, value: q, onChangeText: setQ, placeholder: "Search runs\u2026", "aria-label": "Search runs", onKeyDown: (e) => {
|
|
74
|
+
if ((e.key ?? e.nativeEvent?.key) === 'Enter' && hits[0])
|
|
75
|
+
onOpen(hits[0].id);
|
|
76
|
+
} }), _jsx(XStack, { gap: "$1", flexWrap: "wrap", role: "group", "aria-label": "Status", children: FILTERS.map((f) => (_jsx(XStack, { render: "button", "aria-label": f.label, "aria-pressed": status === f.id, onPress: () => setStatus(f.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: status === f.id ? '$ink' : '$borderColor', bg: status === f.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$1", color: status === f.id ? '$ink' : '$soft', children: f.label }) }, f.id || 'all'))) }), _jsx(ScrollView, { maxH: 360, children: _jsxs(YStack, { gap: "$1", role: "list", children: [hits.map((h) => (_jsxs(XStack, { role: "listitem", render: "button", "aria-label": h.title, onPress: () => onOpen(h.id), px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, items: "center", gap: "$3", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: h.title }), h.line ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, shrink: 0, maxW: "45%", children: h.line })) : null] }, h.id))), !loading && !error && hits.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", children: rows && next ? 'No run read so far matches.' : 'No run matches.' })) : null] }) }), error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: error })) : null, loading ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: "Reading runs\u2026" })) : next ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", onPress: () => void read(next), children: "Older runs" }) })) : null] }) }));
|
|
77
|
+
}
|
package/lib/foot.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const DOCS = "https://docs.hanzo.ai/docs/dev";
|
|
2
|
+
/** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
|
|
3
|
+
export declare function Slack({ onOpen, onDismiss }: {
|
|
4
|
+
onOpen: () => void;
|
|
5
|
+
onDismiss: () => void;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
/**
|
|
8
|
+
* The account menu. The rail draws the account row and says when it was pressed,
|
|
9
|
+
* so the menu hangs off an anchor laid over the rail's foot: above it on the
|
|
10
|
+
* open rail, beside it on the collapsed one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function Who({ open, onOpenChange, narrow }: {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (o: boolean) => void;
|
|
15
|
+
narrow: boolean;
|
|
16
|
+
}): import("react").JSX.Element;
|