babavoss 0.0.1 → 0.0.2
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/NOTICE +7 -0
- package/bin/voss.ts +48 -0
- package/gui/babavoss-web.js +234 -0
- package/gui/chunk-5hpp1ypv.js +11710 -0
- package/gui/chunk-9rq662fd.js +8627 -0
- package/gui/chunk-k3cm8j6r.js +433 -0
- package/gui/chunk-pk09y98y.js +50 -0
- package/gui/chunk-smz02qa6.js +186 -0
- package/gui/chunk-wwqypxre.js +49 -0
- package/gui/gui.js +2015 -0
- package/gui/react-compiler-runtime.js +39 -0
- package/gui/react-dom-client.js +21 -0
- package/gui/react-dom.js +44 -0
- package/gui/react-jsx-runtime.js +19 -0
- package/gui/react.js +105 -0
- package/gui/theme.css +3053 -0
- package/index.ts +15 -0
- package/package.json +50 -4
- package/src/baba/check.ts +90 -0
- package/src/baba/config.ts +261 -0
- package/src/baba/find.ts +12 -0
- package/src/baba/init.ts +176 -0
- package/src/baba/node.ts +483 -0
- package/src/baba/project.ts +63 -0
- package/src/baba/registry.ts +35 -0
- package/src/baba/worker.ts +55 -0
- package/src/bench/index.ts +6 -0
- package/src/bench/measure.ts +132 -0
- package/src/bench/scenarios.ts +136 -0
- package/src/build/builder.ts +74 -0
- package/src/build/failure.ts +78 -0
- package/src/build/guard.ts +85 -0
- package/src/build/mdx-register.ts +3 -0
- package/src/build/mdx.ts +38 -0
- package/src/build/project.ts +38 -0
- package/src/build/views.ts +146 -0
- package/src/builder/main.ts +29 -0
- package/src/desktop/bob.ts +76 -0
- package/src/desktop/desktop.css +111 -0
- package/src/desktop/icons.ts +50 -0
- package/src/desktop/index.ts +323 -0
- package/src/desktop/routes.ts +98 -0
- package/src/desktop/view.tsx +673 -0
- package/src/door/core.ts +384 -0
- package/src/ecs/baba.ts +431 -0
- package/src/ecs/codec.ts +334 -0
- package/src/ecs/handles.ts +91 -0
- package/src/ecs/replica.ts +150 -0
- package/src/ecs/runtime.ts +603 -0
- package/src/ecs/scheduler.ts +75 -0
- package/src/ecs/snapshot.ts +102 -0
- package/src/ecs/state.ts +759 -0
- package/src/ecs/system.ts +256 -0
- package/src/ecs/table.ts +420 -0
- package/src/ecs/testbed.ts +97 -0
- package/src/exec/host.ts +177 -0
- package/src/exec/main.ts +98 -0
- package/src/exec/watch.ts +7 -0
- package/src/exec/wire.ts +29 -0
- package/src/generated/build.ts +4 -0
- package/src/gui/css.d.ts +1 -0
- package/src/gui/gui.tsx +245 -0
- package/src/gui/index.ts +51 -0
- package/src/gui/inspector.tsx +47 -0
- package/src/gui/levels.tsx +73 -0
- package/src/gui/promptware.tsx +68 -0
- package/src/gui/runner.tsx +118 -0
- package/src/gui/theme.css +498 -0
- package/src/gui/theme.ts +25 -0
- package/src/gui/wizard.tsx +227 -0
- package/src/guide/add-a-desktop.mdx +100 -0
- package/src/guide/compose-an-interface.mdx +84 -0
- package/src/guide/index.ts +13 -0
- package/src/guide/reach-outside.mdx +112 -0
- package/src/guide/spec-a-system.mdx +93 -0
- package/src/guide/systems-together.mdx +72 -0
- package/src/guide/write-a-system.mdx +183 -0
- package/src/guide/write-promptware.mdx +90 -0
- package/src/http/server.ts +310 -0
- package/src/kernel/build.ts +21 -0
- package/src/kernel/builder.ts +105 -0
- package/src/kernel/children.ts +117 -0
- package/src/kernel/context.ts +90 -0
- package/src/kernel/lock.ts +46 -0
- package/src/kernel/names.ts +14 -0
- package/src/kernel/schema.ts +130 -0
- package/src/kernel/where.ts +12 -0
- package/src/kit/index.ts +232 -0
- package/src/maker/system.ts +213 -0
- package/src/mcp/daemon.ts +61 -0
- package/src/mcp/main.ts +208 -0
- package/src/mcp/rpc.ts +64 -0
- package/src/mcp/tools.ts +125 -0
- package/src/prompt/evals.ts +42 -0
- package/src/prompt/index.ts +242 -0
- package/src/prompt/jsx-dev-runtime.ts +1 -0
- package/src/prompt/jsx-runtime.ts +49 -0
- package/src/prompt/mdx.d.ts +1 -0
- package/src/promptware/compile.ts +183 -0
- package/src/promptware/define.ts +16 -0
- package/src/promptware/disk.ts +72 -0
- package/src/promptware/markdown.d.ts +6 -0
- package/src/promptware/sync.ts +437 -0
- package/src/promptware/system.ts +215 -0
- package/src/runtime/bridge.ts +85 -0
- package/src/runtime/connect.ts +54 -0
- package/src/runtime/env.ts +35 -0
- package/src/runtime/harness.ts +80 -0
- package/src/runtime/main.ts +119 -0
- package/src/runtime/worker.ts +33 -0
- package/src/server/edge.ts +332 -0
- package/src/server/main.ts +45 -0
- package/src/server/messages.ts +97 -0
- package/src/server/protocol.ts +37 -0
- package/src/services/args.ts +45 -0
- package/src/services/exec.ts +69 -0
- package/src/services/fs.ts +139 -0
- package/src/services/http.ts +30 -0
- package/src/services/index.ts +113 -0
- package/src/services/secrets.ts +18 -0
- package/src/shell/address.ts +21 -0
- package/src/shell/args.ts +219 -0
- package/src/shell/client.ts +107 -0
- package/src/shell/codes.ts +26 -0
- package/src/shell/positional.ts +20 -0
- package/src/shell/run.ts +470 -0
- package/src/shell/service.ts +167 -0
- package/src/shell/state.ts +204 -0
- package/src/spec/adapters.ts +72 -0
- package/src/spec/diff.ts +26 -0
- package/src/spec/files.ts +17 -0
- package/src/spec/index.ts +155 -0
- package/src/spec/run.ts +97 -0
- package/src/spec/take.ts +54 -0
- package/src/test/index.ts +8 -0
- package/src/test/prove.ts +56 -0
- package/src/test/records.ts +23 -0
- package/src/test/specs.ts +56 -0
- package/src/test/steps.ts +100 -0
- package/src/test/voss-dir.ts +17 -0
- package/src/transport/messages.ts +110 -0
- package/src/transport/transport.ts +62 -0
- package/src/wall/probe.ts +67 -0
- package/src/wall/profile.ts +103 -0
- package/src/wall/spawn.ts +59 -0
- package/src/web/app.tsx +53 -0
- package/src/web/core.tsx +140 -0
- package/src/web/form.ts +155 -0
- package/src/web/hooks.ts +135 -0
- package/src/web/index.tsx +17 -0
- package/src/web/list.ts +19 -0
- package/src/web/maker.tsx +766 -0
- package/src/web/objects.tsx +213 -0
- package/src/web/socket.ts +84 -0
- package/src/web/state.tsx +69 -0
- package/src/web/store.ts +221 -0
- package/src/web/ui.tsx +135 -0
- package/README.md +0 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The inspector: the state as it is. Resources, entities with their
|
|
2
|
+
// components, jobs with their status, and the round. Layer zero: a filter
|
|
3
|
+
// by component, and despawn.
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import type { Manifest } from "../ecs/baba.ts";
|
|
6
|
+
import { useBabaState } from "../web/state.tsx";
|
|
7
|
+
import { Button, Input, Table } from "../web/ui.tsx";
|
|
8
|
+
|
|
9
|
+
export function Inspector({ manifest }: { manifest: Manifest }) {
|
|
10
|
+
const { w, raw, ready, round } = useBabaState();
|
|
11
|
+
const [filter, setFilter] = useState("");
|
|
12
|
+
if (!ready) return <p className="bv-muted">Reading the state…</p>;
|
|
13
|
+
const names = Object.keys(manifest.components);
|
|
14
|
+
const wanted = filter.trim();
|
|
15
|
+
const entities = w.all().filter((e) => !wanted || wanted in w.of(e));
|
|
16
|
+
const resources = Object.entries(manifest.resources).map(([n, r]) => ({ name: n, system: r.owner, value: JSON.stringify(w.get({ kind: "resource", name: n } as never)) }));
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<p className="bv-muted">round {round} · {w.all().length} entities · {w.jobs.length} jobs</p>
|
|
20
|
+
{resources.length > 0 && (
|
|
21
|
+
<section className="gui-section"><h2>Resources</h2><Table columns={["name", "system", "value"]} rows={resources} /></section>
|
|
22
|
+
)}
|
|
23
|
+
<section className="gui-section">
|
|
24
|
+
<h2>Entities</h2>
|
|
25
|
+
<div className="gui-filter">
|
|
26
|
+
<Input list="gui-components" placeholder="with component…" value={filter} onChange={(e) => setFilter(e.target.value)} />
|
|
27
|
+
<datalist id="gui-components">{names.map((n) => <option key={n} value={n} />)}</datalist>
|
|
28
|
+
{wanted && <Button variant="quiet" onClick={() => setFilter("")}>clear</Button>}
|
|
29
|
+
</div>
|
|
30
|
+
{entities.length === 0 && <p className="bv-muted">No entity{wanted ? ` with ${wanted}` : ""}.</p>}
|
|
31
|
+
{entities.map((e) => (
|
|
32
|
+
<div key={e} className="gui-entity">
|
|
33
|
+
<code>#{e}</code>
|
|
34
|
+
<div className="gui-parts">
|
|
35
|
+
{Object.entries(w.of(e)).map(([n, v]) => <span key={n} className="gui-part"><b>{n}</b><pre>{v === true ? "" : JSON.stringify(v)}</pre></span>)}
|
|
36
|
+
{w.jobs.filter((j) => j.entity === e).map((j) => <span key={j.effect} className="gui-part bv-muted"><b>{j.effect}</b><pre>{j.status}</pre></span>)}
|
|
37
|
+
</div>
|
|
38
|
+
<Button variant="quiet" onClick={() => void raw({ op: "despawn", entity: e })}>despawn</Button>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</section>
|
|
42
|
+
{w.jobs.length > 0 && (
|
|
43
|
+
<section className="gui-section"><h2>Jobs</h2><Table columns={["effect", "entity", "args", "status"]} rows={w.jobs.map((j) => ({ effect: j.effect, entity: j.entity ?? "baba", args: JSON.stringify(j.args), status: j.status }))} /></section>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// voss's root: the picker. One voss runs on this machine, and its root page
|
|
2
|
+
// lists the babas it knows, to open one, and makes a new one. A page inside
|
|
3
|
+
// a baba is that baba only; Leave brings a tab back here. The root has no
|
|
4
|
+
// state: its shell is the same frame over page state, the babas as its
|
|
5
|
+
// apps, and its one window is the New baba wizard.
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { DesktopFrame, type Picture, type AppRow } from "../desktop/view.tsx";
|
|
8
|
+
import type { IconShape } from "../desktop/icons.ts";
|
|
9
|
+
import type { Hand } from "../web/app.tsx";
|
|
10
|
+
import { theme } from "./theme.ts";
|
|
11
|
+
import { Folders, getJson, type Project } from "./wizard.tsx";
|
|
12
|
+
|
|
13
|
+
/** The babas this machine knows, and its home, from the daemon; null until they arrive. */
|
|
14
|
+
export function useProjects(): { projects: Project[] | null; home: string } {
|
|
15
|
+
const [state, setState] = useState<{ projects: Project[] | null; home: string }>({ projects: null, home: "" });
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let live = true;
|
|
18
|
+
getJson<{ projects: Project[]; home?: string }>("/api/projects").then((j) => { if (live) setState({ projects: j.projects, home: j.home ?? "" }); }, () => { if (live) setState({ projects: [], home: "" }); });
|
|
19
|
+
return () => { live = false; };
|
|
20
|
+
}, []);
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const href = (name: string) => `/${encodeURIComponent(name)}/`;
|
|
25
|
+
const plus: readonly IconShape[] = [{ tag: "path", attrs: { d: "M5 12h14" } }, { tag: "path", attrs: { d: "M12 5v14" } }];
|
|
26
|
+
const eyes: readonly IconShape[] = [{ tag: "circle", attrs: { cx: "8.1", cy: "12", r: "4.3" } }, { tag: "circle", attrs: { cx: "17.2", cy: "12", r: "3" } }];
|
|
27
|
+
/** The wizard's app, the picker's one window. */
|
|
28
|
+
export const NEW = "new";
|
|
29
|
+
/** A baba's app key in the picker; a baba may be called `new`, so the key says which it is. */
|
|
30
|
+
export const babaKey = (name: string) => `baba:${name}`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The picker's rows as the frame's apps: every baba under Babas, a baba inside another under its parent's
|
|
34
|
+
* title, in the order voss lists them; then New baba under Make. Pure: the same from the same list.
|
|
35
|
+
*/
|
|
36
|
+
export function pickerApps(projects: Project[]): AppRow[] {
|
|
37
|
+
const byName = new Map(projects.map((p) => [p.name, p]));
|
|
38
|
+
const rows: AppRow[] = projects.map((p, i) => {
|
|
39
|
+
const parent = p.parent ? byName.get(p.parent) : undefined;
|
|
40
|
+
return { entity: 10 + i, key: babaKey(p.name), title: p.title, group: parent ? `In ${parent.title}` : "Babas", icon: "", many: false, order: i, drawing: eyes };
|
|
41
|
+
});
|
|
42
|
+
rows.push({ entity: 2, key: NEW, title: "New baba", group: "Make", icon: "plus", many: false, order: projects.length, drawing: plus });
|
|
43
|
+
return rows;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Win = Picture["windows"][number];
|
|
47
|
+
const wizard: Win = { entity: 3, session: 1, app: 2, path: "/", title: "", memory: {} };
|
|
48
|
+
|
|
49
|
+
/** The picker: the frame over page state, the babas as its apps; choosing one goes to its page, New baba opens the wizard in front. */
|
|
50
|
+
export function Root() {
|
|
51
|
+
const { projects, home } = useProjects();
|
|
52
|
+
const [open, setOpen] = useState(false);
|
|
53
|
+
const apps = useMemo(() => pickerApps(projects ?? []), [projects]);
|
|
54
|
+
const picture: Picture = {
|
|
55
|
+
session: { entity: 1, key: "main", selected: open ? wizard.entity : null, theme: "system" }, theme: theme(),
|
|
56
|
+
apps,
|
|
57
|
+
windows: open ? [wizard] : [],
|
|
58
|
+
};
|
|
59
|
+
// The shell's verbs, kept in the page: a baba opens as another page; the wizard is the one window here.
|
|
60
|
+
const hand: Hand = { fire: (name, args) => {
|
|
61
|
+
const a = (args ?? {}) as { app?: string; window?: number };
|
|
62
|
+
if (name === "desktop-open") {
|
|
63
|
+
if (a.app === NEW) setOpen(true);
|
|
64
|
+
else if (a.app?.startsWith("baba:")) location.assign(href(a.app.slice(5)));
|
|
65
|
+
} else if (name === "desktop-select") setOpen(a.window === wizard.entity);
|
|
66
|
+
else if (name === "desktop-close") setOpen(false);
|
|
67
|
+
} };
|
|
68
|
+
return <div className="gui-view">
|
|
69
|
+
<DesktopFrame picture={picture} title="voss" hand={hand} render={() => (
|
|
70
|
+
<Folders home={home} onBack={() => setOpen(false)} onOpened={(name) => location.assign(href(name))} />
|
|
71
|
+
)} />
|
|
72
|
+
</div>;
|
|
73
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The promptware drawer: the baba's know-how for agents as it declares it,
|
|
2
|
+
// the harness as the baba declares it, the plan of each output, and a sync.
|
|
3
|
+
// Read from the state by the promptware system's names.
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import type { Manifest } from "../ecs/baba.ts";
|
|
6
|
+
import { useBabaState } from "../web/state.tsx";
|
|
7
|
+
import { Button, Rows, Row, Table } from "../web/ui.tsx";
|
|
8
|
+
import { ErrorText } from "./runner.tsx";
|
|
9
|
+
|
|
10
|
+
interface Plan { auto: boolean; lastSync: number | null; pending: number; error: string | null; outputs: { path: string; harness: string; status: string; action: string | null; reason: string }[] }
|
|
11
|
+
|
|
12
|
+
export function Promptware({ manifest }: { manifest: Manifest }) {
|
|
13
|
+
const { fire, read, ready, round } = useBabaState();
|
|
14
|
+
const [plan, setPlan] = useState<Plan | null>(null);
|
|
15
|
+
const [error, setError] = useState<unknown>(null);
|
|
16
|
+
useEffect(() => { if (ready) read("promptware-plan").then((p) => setPlan(p as Plan), setError); }, [ready, round]);
|
|
17
|
+
if (!("pwStatus" in manifest.resources)) return <p className="bv-muted">This baba has no promptware system.</p>;
|
|
18
|
+
if (!ready) return <p className="bv-muted">Reading the state…</p>;
|
|
19
|
+
const act = (name: string, args: unknown) => { setError(null); return fire(name, args as never).catch((e: unknown) => { setError(e); throw e; }); };
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
{error !== null && <ErrorText error={error} />}
|
|
23
|
+
<Declared manifest={manifest} />
|
|
24
|
+
<section className="gui-section">
|
|
25
|
+
<h2>Outputs</h2>
|
|
26
|
+
<div className="bv-form-actions">
|
|
27
|
+
<Button variant="primary" onClick={() => void act("promptware-sync", {}).catch(() => {})}>Sync now</Button>
|
|
28
|
+
{plan && <span className="bv-muted">{plan.pending ? `${plan.pending} to write` : "up to date"} · {plan.auto ? "writes on every change" : "writes on sync"}{plan.lastSync ? ` · synced ${new Date(plan.lastSync).toLocaleTimeString()}` : ""}</span>}
|
|
29
|
+
</div>
|
|
30
|
+
{plan?.error && <p className="bv-error">{plan.error}</p>}
|
|
31
|
+
{plan && plan.outputs.length > 0 && <Table columns={["path", "harness", "status", "action", "reason"]} rows={plan.outputs.map((o) => ({ ...o, action: o.action ?? "" }))} />}
|
|
32
|
+
</section>
|
|
33
|
+
<Harness manifest={manifest} />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What the baba tells agents, read-only here: it is code under .baba/promptware/. */
|
|
39
|
+
function Declared({ manifest }: { manifest: Manifest }) {
|
|
40
|
+
const pw = manifest.promptware;
|
|
41
|
+
const skills = Object.entries(pw.skills);
|
|
42
|
+
return (
|
|
43
|
+
<section className="gui-section">
|
|
44
|
+
<h2>Promptware</h2>
|
|
45
|
+
{!pw.context && skills.length === 0 && pw.hooks.length === 0 && <p className="bv-muted">Nothing declared yet: give the baba a `context(...)`, a `skill(...)` or a `hook(...)` under `promptware`.</p>}
|
|
46
|
+
{pw.context && <p className="bv-muted">{pw.context.length} characters of context, {pw.hooks.length} hook{pw.hooks.length === 1 ? "" : "s"}.</p>}
|
|
47
|
+
{pw.hooks.length > 0 && <Rows>{pw.hooks.map((h, i) => <Row key={i} hint={`${h.on}${h.match ? ` on ${h.match}` : ""}${h.strict ? " · strict" : ""}`}><code>{h.action}</code></Row>)}</Rows>}
|
|
48
|
+
{skills.length > 0 && <Rows>{skills.map(([k, sk]) => <Row key={k} hint={sk.description}><code>{k}</code></Row>)}</Rows>}
|
|
49
|
+
<p className="bv-muted">The promptware is the baba's code: edit it under .baba/promptware/; the files below follow.</p>
|
|
50
|
+
</section>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The harness as the baba declares it, `baba({ harness })`, read-only. */
|
|
55
|
+
function Harness({ manifest }: { manifest: Manifest }) {
|
|
56
|
+
const h = manifest.harness;
|
|
57
|
+
return (
|
|
58
|
+
<section className="gui-section">
|
|
59
|
+
<h2>Harness</h2>
|
|
60
|
+
<Rows>
|
|
61
|
+
<Row hint={h.claude.enabled ? `permissions: ${h.claude.permissions.join(", ")}` : "off"}><code>claude</code></Row>
|
|
62
|
+
<Row hint={h.codex.enabled ? "on" : "off"}><code>codex</code></Row>
|
|
63
|
+
<Row hint={`${h.auto ? "writes on every change" : "writes on sync"}${h.table ? " · the contract as a table" : ""}`}><code>files</code></Row>
|
|
64
|
+
</Rows>
|
|
65
|
+
<p className="bv-muted">Declared in .baba/index.ts: <code>{"baba({ systems, promptware, harness })"}</code>.</p>
|
|
66
|
+
</section>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// The runner: the contract as forms. A list of the baba's actions and
|
|
2
|
+
// queries; one opens a form generated from its args' schema, fires or
|
|
3
|
+
// reads it and shows what came back. A baba with no view gets this page.
|
|
4
|
+
import { useId, useState, type FormEvent } from "react";
|
|
5
|
+
import type { Manifest } from "../ecs/baba.ts";
|
|
6
|
+
import { useScope, api, ApiError } from "../web/core.tsx";
|
|
7
|
+
import { plan, collect, tableOf, describeType, FieldError, type Field, type Values } from "../web/form.ts";
|
|
8
|
+
import { Button, Input, Textarea, Select, Rows, Row, Table } from "../web/ui.tsx";
|
|
9
|
+
|
|
10
|
+
type Callable = { kind: "fire" | "read"; summary: string; args: import("../kernel/schema.ts").Json; result?: import("../kernel/schema.ts").Json };
|
|
11
|
+
|
|
12
|
+
export function Runner({ manifest }: { manifest: Manifest }) {
|
|
13
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
14
|
+
const calls: Record<string, Callable> = {};
|
|
15
|
+
for (const [n, d] of Object.entries(manifest.actions)) calls[n] = { kind: "fire", summary: d.summary, args: d.args, result: d.result };
|
|
16
|
+
for (const [n, q] of Object.entries(manifest.queries)) calls[n] = { kind: "read", summary: q.summary, args: q.args, result: q.result };
|
|
17
|
+
const names = Object.keys(calls);
|
|
18
|
+
if (!selected) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="bv-runner">
|
|
21
|
+
{names.length === 0 && <p className="bv-muted">The baba declares no actions or queries.</p>}
|
|
22
|
+
<Rows>{names.map((n) => <Row key={n} hint={`${calls[n]!.kind === "fire" ? "action" : "query"} · ${calls[n]!.summary}`} onClick={() => setSelected(n)}><code>{n}</code></Row>)}</Rows>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const def = calls[selected]!;
|
|
27
|
+
return (
|
|
28
|
+
<div className="bv-runner">
|
|
29
|
+
<div className="bv-runner-nav"><Button variant="quiet" onClick={() => setSelected(null)}>← All</Button></div>
|
|
30
|
+
<Form key={selected} name={selected} def={def} />
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Control({ id, f, value, set, invalid }: { id: string; f: Field; value: string | boolean | undefined; set: (v: string | boolean) => void; invalid: boolean }) {
|
|
36
|
+
const text = typeof value === "string" ? value : "";
|
|
37
|
+
const common = { id, "aria-invalid": invalid || undefined, "aria-required": f.required || undefined };
|
|
38
|
+
switch (f.kind) {
|
|
39
|
+
case "boolean":
|
|
40
|
+
return <input type="checkbox" className="bv-check" {...common} checked={value === true} onChange={(e) => set(e.target.checked)} />;
|
|
41
|
+
case "enum":
|
|
42
|
+
return (
|
|
43
|
+
<Select {...common} value={text} onChange={(e) => set(e.target.value)}>
|
|
44
|
+
<option value="">{f.required ? "Choose…" : "(none)"}</option>
|
|
45
|
+
{f.options!.map((o) => <option key={o} value={o}>{o}</option>)}
|
|
46
|
+
</Select>
|
|
47
|
+
);
|
|
48
|
+
case "number":
|
|
49
|
+
case "integer":
|
|
50
|
+
return <Input {...common} type="number" step={f.kind === "integer" ? 1 : "any"} value={text} onChange={(e) => set(e.target.value)} />;
|
|
51
|
+
case "list":
|
|
52
|
+
return <Textarea {...common} rows={3} placeholder={`one ${f.item} per line`} value={text} onChange={(e) => set(e.target.value)} />;
|
|
53
|
+
case "json":
|
|
54
|
+
return <Textarea {...common} rows={4} className="bv-mono" placeholder="JSON" value={text} onChange={(e) => set(e.target.value)} />;
|
|
55
|
+
default:
|
|
56
|
+
return <Input {...common} value={text} onChange={(e) => set(e.target.value)} />;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Outcome = { state: "idle" } | { state: "running" } | { state: "done"; value: unknown; ms: number } | { state: "failed"; error: unknown };
|
|
61
|
+
|
|
62
|
+
function Form({ name, def }: { name: string; def: Callable }) {
|
|
63
|
+
const scope = useScope();
|
|
64
|
+
const uid = useId();
|
|
65
|
+
const p = plan(def.args);
|
|
66
|
+
const [values, setValues] = useState<Values>({});
|
|
67
|
+
const [outcome, setOutcome] = useState<Outcome>({ state: "idle" });
|
|
68
|
+
const run = async (e?: FormEvent) => {
|
|
69
|
+
e?.preventDefault();
|
|
70
|
+
let args: unknown;
|
|
71
|
+
try { args = collect(p, values); } catch (err) { setOutcome({ state: "failed", error: err }); return; }
|
|
72
|
+
setOutcome({ state: "running" });
|
|
73
|
+
const t = performance.now();
|
|
74
|
+
try {
|
|
75
|
+
const value = await api(def.kind, scope, name, args);
|
|
76
|
+
setOutcome({ state: "done", value, ms: Math.round(performance.now() - t) });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setOutcome({ state: "failed", error: err });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const bad = outcome.state === "failed" && outcome.error instanceof FieldError ? outcome.error.field : null;
|
|
82
|
+
return (
|
|
83
|
+
<form className="bv-form" onSubmit={run}>
|
|
84
|
+
<div className="bv-form-head"><code className="bv-form-name">{name}</code><span className="bv-muted">{def.kind === "fire" ? "action" : "query"} · {def.summary}</span></div>
|
|
85
|
+
{p.fields.length === 0 && <p className="bv-muted">No arguments.</p>}
|
|
86
|
+
{p.fields.map((f) => (
|
|
87
|
+
<div key={f.name} className={f.kind === "boolean" ? "bv-field bv-field-inline" : "bv-field"}>
|
|
88
|
+
<label htmlFor={`${uid}-${f.name}`}>
|
|
89
|
+
{f.name}{f.required && <span className="bv-required" title="required">*</span>}
|
|
90
|
+
<span className="bv-type">{describeType(f)}</span>
|
|
91
|
+
</label>
|
|
92
|
+
<Control id={`${uid}-${f.name}`} f={f} value={values[f.name]} set={(v) => setValues((s) => ({ ...s, [f.name]: v }))} invalid={bad === f.name} />
|
|
93
|
+
{f.description && <small className="bv-muted">{f.description}</small>}
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
<div className="bv-form-actions">
|
|
97
|
+
<Button type="submit" variant="primary" disabled={outcome.state === "running"}>{outcome.state === "running" ? "Running…" : def.kind === "fire" ? "Run" : "Read"}</Button>
|
|
98
|
+
{outcome.state === "done" && <span className="bv-muted">{outcome.ms} ms</span>}
|
|
99
|
+
</div>
|
|
100
|
+
{outcome.state === "failed" && <ErrorText error={outcome.error} />}
|
|
101
|
+
{outcome.state === "done" && <Output value={outcome.value} />}
|
|
102
|
+
</form>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function ErrorText({ error }: { error: unknown }) {
|
|
107
|
+
if (error instanceof ApiError) return <p className="bv-error" role="alert"><code className="bv-code">{error.code}</code> {error.message}</p>;
|
|
108
|
+
if (error instanceof FieldError) return <p className="bv-error" role="alert">{error.message}</p>;
|
|
109
|
+
return <p className="bv-error" role="alert">{error instanceof Error ? error.message : String(error)}</p>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A result: a table when it is an array of flat objects, else JSON. */
|
|
113
|
+
export function Output({ value }: { value: unknown }) {
|
|
114
|
+
const t = tableOf(value);
|
|
115
|
+
if (t) return <div className="bv-output"><Table columns={t.columns} rows={t.rows} /></div>;
|
|
116
|
+
if (value === undefined || value === null) return <div className="bv-output bv-muted">Done; nothing came back.</div>;
|
|
117
|
+
return <pre className="bv-output bv-json">{JSON.stringify(value, null, 2)}</pre>;
|
|
118
|
+
}
|