@fungi.computer/agar 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -18
- package/dist/agent-message-meta.d.ts +21 -16
- package/dist/agent-message-meta.js +17 -6
- package/dist/agents.d.ts +86 -13
- package/dist/agents.js +48 -25
- package/dist/approvals.d.ts +5 -0
- package/dist/approvals.js +36 -0
- package/dist/apps.js +4 -4
- package/dist/backend-definition.d.ts +41 -4
- package/dist/backend-definition.js +70 -10
- package/dist/backend-refusal.d.ts +1 -0
- package/dist/backend-refusal.js +1 -0
- package/dist/backend.js +4 -4
- package/dist/channels.js +4 -4
- package/dist/computer-contracts.d.ts +11 -28
- package/dist/computer-contracts.js +10 -29
- package/dist/computer.js +16 -16
- package/dist/connect.d.ts +3 -3
- package/dist/connect.js +7 -17
- package/dist/errors.d.ts +39 -1
- package/dist/errors.js +62 -5
- package/dist/game-servers.js +2 -2
- package/dist/git-rpc.d.ts +23 -23
- package/dist/git-rpc.js +14 -16
- package/dist/git.d.ts +9 -9
- package/dist/git.js +8 -8
- package/dist/host.d.ts +3 -1
- package/dist/host.js +5 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +5 -2
- package/dist/inference.js +2 -1
- package/dist/internal/app-session.js +37 -1
- package/dist/internal/media-diagnostic.d.ts +11 -0
- package/dist/internal/media-diagnostic.js +21 -0
- package/dist/internal/protocol.d.ts +4 -4
- package/dist/internal/schemas.d.ts +40 -7
- package/dist/internal/schemas.js +122 -35
- package/dist/marketplace.js +7 -5
- package/dist/media-json.d.ts +14 -0
- package/dist/media-json.js +1 -0
- package/dist/media-request-id.d.ts +1 -1
- package/dist/media-request-id.js +2 -2
- package/dist/media.d.ts +11 -0
- package/dist/media.js +8 -2
- package/dist/method-presentation.d.ts +7 -0
- package/dist/method-presentation.js +14 -0
- package/dist/method-refusal.d.ts +5 -5
- package/dist/method-refusal.js +4 -4
- package/dist/query.d.ts +2 -2
- package/dist/react-mount.d.ts +7 -1
- package/dist/react-mount.js +28 -8
- package/dist/react.d.ts +14 -2
- package/dist/react.js +40 -4
- package/dist/read-copy.d.ts +1 -1
- package/dist/read-copy.js +3 -6
- package/dist/storage.js +1 -1
- package/dist/test-kit/git.js +19 -2
- package/dist/window.d.ts +68 -0
- package/dist/window.js +31 -0
- package/docs/api/README.md +15 -15
- package/docs/api/backend-definition.md +83 -9
- package/docs/api/backend.md +45 -38
- package/docs/api/errors.md +104 -0
- package/docs/api/git-rpc.md +35 -35
- package/docs/api/git.md +66 -80
- package/docs/api/index.md +851 -143
- package/docs/api/media-json.md +32 -59
- package/docs/api/media-request-id.md +1 -1
- package/docs/api/method-presentation.md +21 -0
- package/docs/api/modules.md +1 -0
- package/docs/api/query.md +1 -1
- package/docs/api/react.md +23 -10
- package/package.json +16 -6
package/dist/method-refusal.js
CHANGED
|
@@ -36,13 +36,13 @@ export class AppMethodRefusal extends Error {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
/** A refused call as it crosses the generated method boundary. */
|
|
39
|
-
export const appMethodRefusedReplySchema = z.strictObject({
|
|
39
|
+
export const appMethodRefusedReplySchema = z.lazy(() => z.strictObject({
|
|
40
40
|
kind: z.literal("refused"),
|
|
41
41
|
code: z.enum(appMethodRefusalCodes),
|
|
42
42
|
message: z.string().min(1).max(maxAppMethodRefusalMessage),
|
|
43
|
-
});
|
|
43
|
+
}));
|
|
44
44
|
/** Every generated App method RPC answers with exactly one of these. */
|
|
45
|
-
export const appMethodReplySchema = z.discriminatedUnion("kind", [
|
|
45
|
+
export const appMethodReplySchema = z.lazy(() => z.discriminatedUnion("kind", [
|
|
46
46
|
z.strictObject({ kind: z.literal("ok"), value: z.json() }),
|
|
47
47
|
appMethodRefusedReplySchema,
|
|
48
|
-
]);
|
|
48
|
+
]));
|
package/dist/query.d.ts
CHANGED
|
@@ -54,9 +54,9 @@ export declare function useMemberInbox(app: QueryApp | null): {
|
|
|
54
54
|
readonly nodeId: string;
|
|
55
55
|
readonly iteration: string;
|
|
56
56
|
};
|
|
57
|
-
readonly state: "pending" | "
|
|
58
|
-
readonly workflowId: string | null;
|
|
57
|
+
readonly state: "pending" | "answered" | "closed" | "delivered" | "timed_out";
|
|
59
58
|
readonly expiresAt: number;
|
|
59
|
+
readonly workflowId: string | null;
|
|
60
60
|
readonly installationId: string;
|
|
61
61
|
readonly workflowTitle: string;
|
|
62
62
|
readonly reviewTitle: string;
|
package/dist/react-mount.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { type QueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
+
import { AppError } from "./errors.js";
|
|
3
4
|
import type { AppRoot } from "./root.js";
|
|
4
5
|
import type { Disposer } from "./store.js";
|
|
5
6
|
/** The App connection state passed to `mountApp`'s `render` callback:
|
|
6
7
|
* `live` once connected, or `connecting`/`standalone`/`ended` while there
|
|
7
|
-
* is no usable App.
|
|
8
|
+
* is no usable App. An embedded page whose handshake fails is `not-hosted`,
|
|
9
|
+
* never standalone; Agar also renders that failure visibly. */
|
|
8
10
|
export type AppMountState = Readonly<{
|
|
9
11
|
app: AppRoot;
|
|
10
12
|
status: "live";
|
|
11
13
|
} | {
|
|
12
14
|
app: null;
|
|
13
15
|
status: "connecting" | "standalone" | "ended";
|
|
16
|
+
} | {
|
|
17
|
+
app: null;
|
|
18
|
+
status: "not-hosted";
|
|
19
|
+
error: AppError;
|
|
14
20
|
}>;
|
|
15
21
|
/** Options passed to `mountApp`. */
|
|
16
22
|
export type AppMountOptions = Readonly<{
|
package/dist/react-mount.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyTheme, getTheme } from "@fungi.computer/stipe/theme";
|
|
2
2
|
import { QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
-
import { createElement, useLayoutEffect } from "react";
|
|
3
|
+
import { createElement, Fragment, useLayoutEffect, } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { connectApp } from "./connect.js";
|
|
6
6
|
import { AppError } from "./errors.js";
|
|
@@ -37,17 +37,25 @@ export function mountApp(element, options, render) {
|
|
|
37
37
|
themed = false;
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
-
const {
|
|
40
|
+
const { defaultView } = element.ownerDocument;
|
|
41
41
|
performance.mark("fungi.app-open.react-root-render-start");
|
|
42
|
-
if (!defaultView
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
if (!defaultView) {
|
|
43
|
+
publish({
|
|
44
|
+
app: null,
|
|
45
|
+
status: "not-hosted",
|
|
46
|
+
error: new AppError("The App document has no window", {
|
|
47
|
+
code: "DISCONNECTED",
|
|
48
|
+
retryable: false,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (defaultView.parent === defaultView) {
|
|
45
53
|
publish({ app: null, status: "standalone" });
|
|
46
54
|
}
|
|
47
55
|
else {
|
|
48
56
|
publish({ app: null, status: "connecting" });
|
|
49
57
|
void connectApp({
|
|
50
|
-
|
|
58
|
+
host: "parent",
|
|
51
59
|
signal: controller.signal,
|
|
52
60
|
})
|
|
53
61
|
.then((app) => {
|
|
@@ -85,7 +93,17 @@ export function mountApp(element, options, render) {
|
|
|
85
93
|
console.error(`${options.name} host connection failed`, cause);
|
|
86
94
|
releaseConnection();
|
|
87
95
|
if (!controller.signal.aborted)
|
|
88
|
-
publish({
|
|
96
|
+
publish({
|
|
97
|
+
app: null,
|
|
98
|
+
status: "not-hosted",
|
|
99
|
+
error: cause instanceof AppError
|
|
100
|
+
? cause
|
|
101
|
+
: new AppError("The App host is unavailable", {
|
|
102
|
+
cause,
|
|
103
|
+
code: "HOST_FAILURE",
|
|
104
|
+
retryable: true,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
89
107
|
});
|
|
90
108
|
}
|
|
91
109
|
return () => {
|
|
@@ -104,5 +122,7 @@ function AppView({ state, render, }) {
|
|
|
104
122
|
if (state.app)
|
|
105
123
|
performance.mark("fungi.app-open.react-connected-commit");
|
|
106
124
|
}, [state.app]);
|
|
107
|
-
return
|
|
125
|
+
return createElement(Fragment, null, state.status === "not-hosted"
|
|
126
|
+
? createElement("p", { role: "alert" }, "App host connection unavailable. Open this App through its host to try again.")
|
|
127
|
+
: null, render(state));
|
|
108
128
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type WhistleContribution, type WhistleRuntime } from "@fungi.computer/whistle";
|
|
2
|
-
import type { AppBackHandler, HostNavigation } from "./window.js";
|
|
2
|
+
import type { AppBackHandler, AppCrumb, AppHeaderCommand, AppHeaderStatus, HostNavigation } from "./window.js";
|
|
3
3
|
import type { AppContext } from "./lifecycle.js";
|
|
4
4
|
import type { AppStatusActionHandler, AppStatusItem } from "./ui.js";
|
|
5
5
|
import type { AppRoot } from "./root.js";
|
|
@@ -31,11 +31,23 @@ export type HostNavigationOptions = Readonly<{
|
|
|
31
31
|
canGoBack: boolean;
|
|
32
32
|
/** The current page's title, shown after the App name in window chrome. */
|
|
33
33
|
title?: string | undefined;
|
|
34
|
+
/** The App's path, shown in the window header in place of the title. */
|
|
35
|
+
crumbs?: readonly AppCrumb[] | undefined;
|
|
36
|
+
/** The view's main commands, as header buttons. */
|
|
37
|
+
primary?: readonly AppHeaderCommand[] | undefined;
|
|
38
|
+
/** Less common commands, behind the header's overflow menu. */
|
|
39
|
+
menu?: readonly AppHeaderCommand[] | undefined;
|
|
40
|
+
/** Facts about the view, shown as muted text with a status bead. */
|
|
41
|
+
status?: readonly AppHeaderStatus[] | undefined;
|
|
34
42
|
onBack: AppBackHandler;
|
|
43
|
+
/** The person chose a crumb, or a sibling from a switcher crumb. */
|
|
44
|
+
onCrumb?: ((crumbId: string, siblingId?: string) => void) | undefined;
|
|
45
|
+
/** The person used a primary or menu command. */
|
|
46
|
+
onAction?: ((actionId: string) => void) | undefined;
|
|
35
47
|
}>;
|
|
36
48
|
/**
|
|
37
49
|
* Publish an App's navigation state to its host and own the host's Back
|
|
38
50
|
* requests while mounted. Pass `app?.window.navigation`; with no connected
|
|
39
51
|
* App it does nothing. Unmounting withdraws Back from the host chrome.
|
|
40
52
|
*/
|
|
41
|
-
export declare function useHostNavigation(navigation: HostNavigation | null | undefined, { canGoBack, title, onBack }: HostNavigationOptions): void;
|
|
53
|
+
export declare function useHostNavigation(navigation: HostNavigation | null | undefined, { canGoBack, title, crumbs, primary, menu, status, onBack, onCrumb, onAction, }: HostNavigationOptions): void;
|
package/dist/react.js
CHANGED
|
@@ -80,26 +80,62 @@ export function useAppCommandCatalog(app, { sourceId, namespace }, build) {
|
|
|
80
80
|
* requests while mounted. Pass `app?.window.navigation`; with no connected
|
|
81
81
|
* App it does nothing. Unmounting withdraws Back from the host chrome.
|
|
82
82
|
*/
|
|
83
|
-
export function useHostNavigation(navigation, { canGoBack, title, onBack }) {
|
|
83
|
+
export function useHostNavigation(navigation, { canGoBack, title, crumbs, primary, menu, status, onBack, onCrumb, onAction, }) {
|
|
84
84
|
const latestBack = useRef(onBack);
|
|
85
|
+
const latestCrumb = useRef(onCrumb);
|
|
86
|
+
const latestAction = useRef(onAction);
|
|
85
87
|
useEffect(() => {
|
|
86
88
|
latestBack.current = onBack;
|
|
87
|
-
|
|
89
|
+
latestCrumb.current = onCrumb;
|
|
90
|
+
latestAction.current = onAction;
|
|
91
|
+
}, [onBack, onCrumb, onAction]);
|
|
88
92
|
useEffect(() => {
|
|
89
93
|
if (!navigation)
|
|
90
94
|
return undefined;
|
|
91
95
|
const release = navigation.onBack(() => latestBack.current());
|
|
96
|
+
const releaseActivation = navigation.onActivate((activation) => {
|
|
97
|
+
if (activation.kind === "action") {
|
|
98
|
+
const handler = latestAction.current;
|
|
99
|
+
handler?.(activation.actionId);
|
|
100
|
+
return handler !== undefined;
|
|
101
|
+
}
|
|
102
|
+
const handler = latestCrumb.current;
|
|
103
|
+
if (activation.siblingId === undefined)
|
|
104
|
+
handler?.(activation.crumbId);
|
|
105
|
+
else
|
|
106
|
+
handler?.(activation.crumbId, activation.siblingId);
|
|
107
|
+
return handler !== undefined;
|
|
108
|
+
});
|
|
92
109
|
return () => {
|
|
93
110
|
release();
|
|
111
|
+
releaseActivation();
|
|
94
112
|
report(navigation, { canGoBack: false });
|
|
95
113
|
};
|
|
96
114
|
}, [navigation]);
|
|
115
|
+
// Callers pass fresh arrays each render; the report follows their content.
|
|
116
|
+
const lists = JSON.stringify([crumbs, primary, menu, status]);
|
|
117
|
+
const latestLists = useRef({ crumbs, primary, menu, status });
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
latestLists.current = { crumbs, primary, menu, status };
|
|
120
|
+
}, [crumbs, primary, menu, status]);
|
|
97
121
|
// Each change replaces the last report; only unmount withdraws Back.
|
|
98
122
|
useEffect(() => {
|
|
99
123
|
if (!navigation)
|
|
100
124
|
return;
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
let state = { canGoBack };
|
|
126
|
+
if (title !== undefined)
|
|
127
|
+
state = { ...state, title };
|
|
128
|
+
const current = latestLists.current;
|
|
129
|
+
if (current.crumbs !== undefined)
|
|
130
|
+
state = { ...state, crumbs: current.crumbs };
|
|
131
|
+
if (current.primary !== undefined)
|
|
132
|
+
state = { ...state, primary: current.primary };
|
|
133
|
+
if (current.menu !== undefined)
|
|
134
|
+
state = { ...state, menu: current.menu };
|
|
135
|
+
if (current.status !== undefined)
|
|
136
|
+
state = { ...state, status: current.status };
|
|
137
|
+
report(navigation, state);
|
|
138
|
+
}, [navigation, canGoBack, title, lists]);
|
|
103
139
|
}
|
|
104
140
|
function report(navigation, state) {
|
|
105
141
|
navigation.publish(state).catch((cause) => {
|
package/dist/read-copy.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export declare function countingGit(binding: GitBinding, options: {
|
|
|
52
52
|
delete(name: string): Promise<boolean>;
|
|
53
53
|
log(name: string, options: {
|
|
54
54
|
ref: string;
|
|
55
|
-
limit
|
|
55
|
+
limit? /** Bind a namespace to its maintained App decoder. */: number;
|
|
56
56
|
offset?: number;
|
|
57
57
|
}): Promise<import("./git.js").GitCommit[]>;
|
|
58
58
|
readCommit(name: string, hash: string): Promise<import("./git.js").GitCommit | null>;
|
package/dist/read-copy.js
CHANGED
|
@@ -44,18 +44,15 @@ export function countingGit(binding, options) {
|
|
|
44
44
|
/** Dispatch counts by maintained Git operation. */ counts: () => Object.fromEntries(counts),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
const rowSchema = z.strictObject({
|
|
47
|
+
const rowSchema = z.lazy(() => z.strictObject({
|
|
48
48
|
head: gitSha1Schema.nullable(),
|
|
49
49
|
version: z.number().int(),
|
|
50
50
|
revision: z.number().int(),
|
|
51
51
|
value: z.string().nullable(),
|
|
52
|
-
});
|
|
52
|
+
}));
|
|
53
53
|
const name = z.string().min(1).max(1024);
|
|
54
54
|
const zero = "0".repeat(40);
|
|
55
|
-
const blobHash = z.union([
|
|
56
|
-
gitSha1Schema,
|
|
57
|
-
z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
58
|
-
]);
|
|
55
|
+
const blobHash = z.lazy(() => z.union([gitSha1Schema, z.string().regex(/^sha256:[0-9a-f]{64}$/)]));
|
|
59
56
|
/** Own bounded rows and content-addressed bytes in the App facet, not a host/provider cache. */
|
|
60
57
|
export function createReadCopies(sql, limits) {
|
|
61
58
|
z.strictObject({
|
package/dist/storage.js
CHANGED
|
@@ -140,4 +140,4 @@ function client(raw) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
/** This App's own declared data. The host seals App and member identities. */
|
|
143
|
-
export const appStorage = defineCapability("app.storage", 1, rawSchema.transform(client));
|
|
143
|
+
export const appStorage = defineCapability("app.storage", 1, z.lazy(() => rawSchema.transform(client)));
|
package/dist/test-kit/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
2
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import * as z from "zod";
|
|
@@ -13,7 +13,21 @@ export function realGit() {
|
|
|
13
13
|
const info = (name) => existsSync(join(root, `${name}.git`))
|
|
14
14
|
? { name, defaultBranch: "main", lastPushAt: null, source: null }
|
|
15
15
|
: null;
|
|
16
|
+
// A full object id names an immutable commit, so one read serves every
|
|
17
|
+
// later ask; refs and abbreviations move and are never cached.
|
|
18
|
+
const commits = new Map();
|
|
19
|
+
const immutable = (hash) => /^[0-9a-f]{40,64}$/u.test(hash);
|
|
16
20
|
const readCommit = (name, hash) => {
|
|
21
|
+
// The repository's directory identity keeps a deleted-and-recreated name
|
|
22
|
+
// from serving the old repository's commits, however it was removed.
|
|
23
|
+
const dir = statSync(join(root, `${name}.git`), {
|
|
24
|
+
bigint: true,
|
|
25
|
+
throwIfNoEntry: false,
|
|
26
|
+
});
|
|
27
|
+
const key = `${name}\0${String(dir?.ino)}\0${String(dir?.birthtimeNs)}\0${hash}`;
|
|
28
|
+
const cached = immutable(hash) ? commits.get(key) : undefined;
|
|
29
|
+
if (cached !== undefined)
|
|
30
|
+
return structuredClone(cached);
|
|
17
31
|
const found = spawnSync("git", [`--git-dir=${join(root, `${name}.git`)}`, "cat-file", "-t", hash], { encoding: "utf8" });
|
|
18
32
|
if (found.status !== 0 || found.stdout.trim() !== "commit")
|
|
19
33
|
return null;
|
|
@@ -31,7 +45,7 @@ export function realGit() {
|
|
|
31
45
|
z.string(),
|
|
32
46
|
])
|
|
33
47
|
.parse(run(name, "show", "-s", "--format=%H%x00%T%x00%P%x00%an%x00%ae%x00%cn%x00%ce%x00%at%x00%ct%x00%B", hash).split("\0"));
|
|
34
|
-
|
|
48
|
+
const commit = {
|
|
35
49
|
hash: oid,
|
|
36
50
|
treeHash,
|
|
37
51
|
parents: parents ? parents.split(" ") : [],
|
|
@@ -41,6 +55,9 @@ export function realGit() {
|
|
|
41
55
|
committedAt: Number(committedAt),
|
|
42
56
|
message,
|
|
43
57
|
};
|
|
58
|
+
if (immutable(hash))
|
|
59
|
+
commits.set(key, structuredClone(commit));
|
|
60
|
+
return commit;
|
|
44
61
|
};
|
|
45
62
|
const git = {
|
|
46
63
|
cloneUrl: (name) => {
|
package/dist/window.d.ts
CHANGED
|
@@ -9,6 +9,58 @@ export type BeforeAppClose = () => boolean | Promise<boolean>;
|
|
|
9
9
|
export declare const APP_NAVIGATION_CONTRACT_VERSION: 1;
|
|
10
10
|
/** The longest page title a host shows beside the App's own name. */
|
|
11
11
|
export declare const APP_NAVIGATION_TITLE_MAX = 80;
|
|
12
|
+
/** The most crumbs one navigation report may carry. */
|
|
13
|
+
export declare const APP_NAVIGATION_CRUMBS_MAX = 8;
|
|
14
|
+
/** The most siblings one switcher crumb may offer. */
|
|
15
|
+
export declare const APP_NAVIGATION_SIBLINGS_MAX = 16;
|
|
16
|
+
/** The most primary header commands one report may carry. */
|
|
17
|
+
export declare const APP_NAVIGATION_PRIMARY_MAX = 3;
|
|
18
|
+
/** The most header status facts one report may carry. */
|
|
19
|
+
export declare const APP_NAVIGATION_STATUS_MAX = 4;
|
|
20
|
+
/** The most ⋯ menu commands one report may carry. */
|
|
21
|
+
export declare const APP_NAVIGATION_MENU_MAX = 12;
|
|
22
|
+
/** The longest crumb or command label a host shows. */
|
|
23
|
+
export declare const APP_NAVIGATION_LABEL_MAX = 80;
|
|
24
|
+
/** One place in the App's path; `siblings` makes it a switcher. */
|
|
25
|
+
export type AppCrumb = Readonly<{
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
siblings?: readonly Readonly<{
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}>[];
|
|
32
|
+
}>;
|
|
33
|
+
/** A command the App offers in its window header; the App owns its effect. */
|
|
34
|
+
export type AppHeaderCommand = Readonly<{
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
/** Supported icon-only presentation; label remains the accessible name. */
|
|
38
|
+
icon?: "play";
|
|
39
|
+
/** The Caps button a primary command wears; default `outline`. */
|
|
40
|
+
variant?: "primary" | "outline" | "ghost";
|
|
41
|
+
/** A menu command that removes or discards something. */
|
|
42
|
+
tone?: "destructive";
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
/** A quiet fact about the view, shown as muted text with a status bead. */
|
|
46
|
+
export type AppHeaderStatus = Readonly<{
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
/** Show the bead alone, with label retained as accessible text and a hint. */
|
|
50
|
+
presentation?: "bead";
|
|
51
|
+
tone?: "neutral" | "info" | "warning" | "success" | "error";
|
|
52
|
+
}>;
|
|
53
|
+
/** The person used a crumb (or a switcher's sibling) or a header command. */
|
|
54
|
+
export type AppNavigationActivation = Readonly<{
|
|
55
|
+
kind: "crumb";
|
|
56
|
+
crumbId: string;
|
|
57
|
+
siblingId?: string;
|
|
58
|
+
}> | Readonly<{
|
|
59
|
+
kind: "action";
|
|
60
|
+
actionId: string;
|
|
61
|
+
}>;
|
|
62
|
+
/** Handles one activation; `true` when the App acted on it. */
|
|
63
|
+
export type AppNavigationActivationHandler = (activation: AppNavigationActivation) => boolean | Promise<boolean>;
|
|
12
64
|
/**
|
|
13
65
|
* What the App's own router can do right now. The App owns its history; the
|
|
14
66
|
* host only offers a Back affordance and relays the person's request.
|
|
@@ -17,6 +69,14 @@ export type AppNavigationState = Readonly<{
|
|
|
17
69
|
canGoBack: boolean;
|
|
18
70
|
/** The current page's title, shown after the App name in window chrome. */
|
|
19
71
|
title?: string;
|
|
72
|
+
/** The App's path; the host shows it in place of the App name and title. */
|
|
73
|
+
crumbs?: readonly AppCrumb[];
|
|
74
|
+
/** The view's main commands, as buttons in the header. */
|
|
75
|
+
primary?: readonly AppHeaderCommand[];
|
|
76
|
+
/** Less common commands, behind the header's overflow menu. */
|
|
77
|
+
menu?: readonly AppHeaderCommand[];
|
|
78
|
+
/** Facts about the view, such as "Valid" or "Saved". Data only. */
|
|
79
|
+
status?: readonly AppHeaderStatus[];
|
|
20
80
|
}>;
|
|
21
81
|
/** Go back one step in the App's own history; `true` when it moved. */
|
|
22
82
|
export type AppBackHandler = () => boolean | Promise<boolean>;
|
|
@@ -26,6 +86,8 @@ export type HostNavigation = Readonly<{
|
|
|
26
86
|
publish(state: AppNavigationState): Promise<void>;
|
|
27
87
|
/** Own the host's Back requests; one owner at a time. */
|
|
28
88
|
onBack(handler: AppBackHandler): Disposer;
|
|
89
|
+
/** Own the host's crumb and command activations; one owner at a time. */
|
|
90
|
+
onActivate(handler: AppNavigationActivationHandler): Disposer;
|
|
29
91
|
}>;
|
|
30
92
|
/** App-owned presentation policy consulted before a user closes its window. */
|
|
31
93
|
export type AppWindow = Readonly<{
|
|
@@ -39,3 +101,9 @@ export type AppWindow = Readonly<{
|
|
|
39
101
|
* accepts every report and never asks the App to go back.
|
|
40
102
|
*/
|
|
41
103
|
export declare const inertHostNavigation: HostNavigation;
|
|
104
|
+
/**
|
|
105
|
+
* Whether an activation names something the App published now: a crumb (and
|
|
106
|
+
* its sibling), or an enabled command. Every hop applies this rule against
|
|
107
|
+
* its own latest copy of the report, so a stale shell cannot reach the App.
|
|
108
|
+
*/
|
|
109
|
+
export declare function navigationActivationAllowed(state: AppNavigationState, activation: AppNavigationActivation): boolean;
|
package/dist/window.js
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
export const APP_NAVIGATION_CONTRACT_VERSION = 1;
|
|
6
6
|
/** The longest page title a host shows beside the App's own name. */
|
|
7
7
|
export const APP_NAVIGATION_TITLE_MAX = 80;
|
|
8
|
+
/** The most crumbs one navigation report may carry. */
|
|
9
|
+
export const APP_NAVIGATION_CRUMBS_MAX = 8;
|
|
10
|
+
/** The most siblings one switcher crumb may offer. */
|
|
11
|
+
export const APP_NAVIGATION_SIBLINGS_MAX = 16;
|
|
12
|
+
/** The most primary header commands one report may carry. */
|
|
13
|
+
export const APP_NAVIGATION_PRIMARY_MAX = 3;
|
|
14
|
+
/** The most header status facts one report may carry. */
|
|
15
|
+
export const APP_NAVIGATION_STATUS_MAX = 4;
|
|
16
|
+
/** The most ⋯ menu commands one report may carry. */
|
|
17
|
+
export const APP_NAVIGATION_MENU_MAX = 12;
|
|
18
|
+
/** The longest crumb or command label a host shows. */
|
|
19
|
+
export const APP_NAVIGATION_LABEL_MAX = 80;
|
|
8
20
|
/**
|
|
9
21
|
* Navigation for a root with no host chrome, such as a workbench fixture: it
|
|
10
22
|
* accepts every report and never asks the App to go back.
|
|
@@ -12,4 +24,23 @@ export const APP_NAVIGATION_TITLE_MAX = 80;
|
|
|
12
24
|
export const inertHostNavigation = Object.freeze({
|
|
13
25
|
publish: () => Promise.resolve(),
|
|
14
26
|
onBack: () => () => undefined,
|
|
27
|
+
onActivate: () => () => undefined,
|
|
15
28
|
});
|
|
29
|
+
/**
|
|
30
|
+
* Whether an activation names something the App published now: a crumb (and
|
|
31
|
+
* its sibling), or an enabled command. Every hop applies this rule against
|
|
32
|
+
* its own latest copy of the report, so a stale shell cannot reach the App.
|
|
33
|
+
*/
|
|
34
|
+
export function navigationActivationAllowed(state, activation) {
|
|
35
|
+
if (activation.kind === "action") {
|
|
36
|
+
const command = [...(state.primary ?? []), ...(state.menu ?? [])].find((candidate) => candidate.id === activation.actionId);
|
|
37
|
+
return command !== undefined && command.disabled !== true;
|
|
38
|
+
}
|
|
39
|
+
const crumb = state.crumbs?.find((candidate) => candidate.id === activation.crumbId);
|
|
40
|
+
if (crumb === undefined)
|
|
41
|
+
return false;
|
|
42
|
+
if (activation.siblingId === undefined)
|
|
43
|
+
return true;
|
|
44
|
+
return (crumb.siblings?.some((sibling) => sibling.id === activation.siblingId) ===
|
|
45
|
+
true);
|
|
46
|
+
}
|
package/docs/api/README.md
CHANGED
|
@@ -6,21 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
# Agar API overview
|
|
8
8
|
|
|
9
|
-
| Import | Export
|
|
10
|
-
| ------------------------------------------ |
|
|
11
|
-
| `@fungi.computer/agar` | `connectApp`
|
|
12
|
-
| `@fungi.computer/agar` | `defineCapability`, `AppError`
|
|
13
|
-
| `@fungi.computer/agar/host` | `connectAppHost`
|
|
14
|
-
| `@fungi.computer/agar/react` | `mountApp`, `useAppContext`
|
|
15
|
-
| `@fungi.computer/agar/query` | `queryKeys`
|
|
16
|
-
| `@fungi.computer/agar/errors` | `describeAppError`
|
|
17
|
-
| `@fungi.computer/agar/backend-definition` | `defineAppBackend`, `defineAppMethod`
|
|
18
|
-
| `@fungi.computer/agar/backend-declaration` | `buildAppApi`, `buildAppDeclaration`
|
|
19
|
-
| `@fungi.computer/agar/backend` | `appBackend`
|
|
20
|
-
| `@fungi.computer/agar/rpc` | `rpcHandleSchema`, `rpcMethod`
|
|
21
|
-
| `@fungi.computer/agar/files` | `readAppFile`, `writeAppFile`
|
|
22
|
-
| `@fungi.computer/agar/storage` | `appStorage`
|
|
23
|
-
| `@fungi.computer/agar/computer-contracts` | `
|
|
9
|
+
| Import | Export | Purpose |
|
|
10
|
+
| ------------------------------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `@fungi.computer/agar` | `connectApp` | Connect a host-mounted App frontend and receive its context and grants. |
|
|
12
|
+
| `@fungi.computer/agar` | `defineCapability`, `AppError` | Declare a typed host capability; the SDK's one typed failure. |
|
|
13
|
+
| `@fungi.computer/agar/host` | `connectAppHost` | Mount an App in an author-owned iframe test or standalone page. |
|
|
14
|
+
| `@fungi.computer/agar/react` | `mountApp`, `useAppContext` | Mount a React App and read its host context. |
|
|
15
|
+
| `@fungi.computer/agar/query` | `queryKeys` | TanStack Query keys and live feeds for host data. |
|
|
16
|
+
| `@fungi.computer/agar/errors` | `describeAppError` | Turn a failure into approved, action-hinted UI copy. |
|
|
17
|
+
| `@fungi.computer/agar/backend-definition` | `defineAppBackend`, `defineAppMethod` | Define typed backend methods once, with Zod input and output. |
|
|
18
|
+
| `@fungi.computer/agar/backend-declaration` | `buildAppApi`, `buildAppDeclaration` | Project the backend definition to the JSON Schema App API at build time. |
|
|
19
|
+
| `@fungi.computer/agar/backend` | `appBackend` | Call the App's own backend from its frontend. |
|
|
20
|
+
| `@fungi.computer/agar/rpc` | `rpcHandleSchema`, `rpcMethod` | Validate object-capability handles received over Cap'n Web. |
|
|
21
|
+
| `@fungi.computer/agar/files` | `readAppFile`, `writeAppFile` | Turn a host-owned file read or write handle into a `File` or an upload. |
|
|
22
|
+
| `@fungi.computer/agar/storage` | `appStorage` | Read, write and watch the App's own member and Team data. |
|
|
23
|
+
| `@fungi.computer/agar/computer-contracts` | `appComputerAdmissionRefusalSchema`, `appComputerExecTimeoutSchema` | Validate public Computer admission, dispatch and budget outcomes without exposing host credentials. |
|
|
24
24
|
|
|
25
25
|
A host decides what an App may do: the frontend asks for a capability and the
|
|
26
26
|
host grants, denies or revokes it. The backend definition is runtime-neutral;
|
|
@@ -30,6 +30,9 @@ type MethodPolicy = Readonly<{
|
|
|
30
30
|
hidden?: true;
|
|
31
31
|
anonymous?: true;
|
|
32
32
|
pending?: true;
|
|
33
|
+
present?: Readonly<{
|
|
34
|
+
verb: string;
|
|
35
|
+
}>;
|
|
33
36
|
}>;
|
|
34
37
|
```
|
|
35
38
|
|
|
@@ -37,6 +40,44 @@ A method's schemas own both its TypeScript contract and JSON boundary.
|
|
|
37
40
|
|
|
38
41
|
---
|
|
39
42
|
|
|
43
|
+
### ResultRowCodec
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
type ResultRowCodec = Readonly<{
|
|
47
|
+
decode: (row) => ResultRow;
|
|
48
|
+
schema: z.ZodType;
|
|
49
|
+
}>;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Decodes and bounds result rows; see `resultRows` in
|
|
53
|
+
`@fungi.computer/agar/method-presentation`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### MethodPresentation
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type MethodPresentation<I, O> = Readonly<{
|
|
61
|
+
verb: string;
|
|
62
|
+
row: (call) => ResultRow;
|
|
63
|
+
rows: ResultRowCodec;
|
|
64
|
+
}>;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
How a finished call reads to a person: a pure projection run in the App backend.
|
|
68
|
+
`rows` is `resultRows` from `@fungi.computer/agar/method-presentation`; only a
|
|
69
|
+
backend that presents imports it, so one without presentations carries no row
|
|
70
|
+
decoder.
|
|
71
|
+
|
|
72
|
+
#### Type Parameters
|
|
73
|
+
|
|
74
|
+
| Type Parameter |
|
|
75
|
+
| -------------- |
|
|
76
|
+
| `I` |
|
|
77
|
+
| `O` |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
40
81
|
### AppBackendMethod
|
|
41
82
|
|
|
42
83
|
```ts
|
|
@@ -44,6 +85,8 @@ type AppBackendMethod<Context> = Readonly<{
|
|
|
44
85
|
policy: MethodPolicy;
|
|
45
86
|
input: z.ZodType;
|
|
46
87
|
output: z.ZodType;
|
|
88
|
+
present?: (input, output) => ResultRow;
|
|
89
|
+
presentRow?: z.ZodType;
|
|
47
90
|
call: (input, context, caller) => Promise<WhistleJsonValue>;
|
|
48
91
|
}>;
|
|
49
92
|
```
|
|
@@ -58,6 +101,24 @@ Prepared method export consumed by the backend and its build.
|
|
|
58
101
|
|
|
59
102
|
---
|
|
60
103
|
|
|
104
|
+
### AppBackendMethods
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
type AppBackendMethods<Context> = Readonly<
|
|
108
|
+
Record<string, AppBackendMethod<Context>>
|
|
109
|
+
>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The prepared methods of one backend, keyed by their public names.
|
|
113
|
+
|
|
114
|
+
#### Type Parameters
|
|
115
|
+
|
|
116
|
+
| Type Parameter | Default type |
|
|
117
|
+
| -------------- | ------------ |
|
|
118
|
+
| `Context` | `void` |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
61
122
|
### AppBackendClass
|
|
62
123
|
|
|
63
124
|
```ts
|
|
@@ -123,18 +184,31 @@ EVENTS.emit acknowledges delivery with native void, not a JSON value.
|
|
|
123
184
|
### appSendBudgetRefusalSchema
|
|
124
185
|
|
|
125
186
|
```ts
|
|
126
|
-
const appSendBudgetRefusalSchema:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
187
|
+
const appSendBudgetRefusalSchema: ZodLazy<
|
|
188
|
+
ZodObject<
|
|
189
|
+
{
|
|
190
|
+
error: ZodLiteral<"send_budget_exceeded">;
|
|
191
|
+
retryAfterMs: ZodNumber;
|
|
192
|
+
},
|
|
193
|
+
$strict
|
|
194
|
+
>
|
|
132
195
|
>;
|
|
133
196
|
```
|
|
134
197
|
|
|
135
198
|
App-originated Session send refusal; retry advice is a duration, not a
|
|
136
199
|
timestamp.
|
|
137
200
|
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### presentMethodName
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const presentMethodName: "present" = "present";
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Name of the generated, hidden read method that projects a finished call into its
|
|
210
|
+
row.
|
|
211
|
+
|
|
138
212
|
## Functions
|
|
139
213
|
|
|
140
214
|
### defineAppMethod()
|
|
@@ -155,9 +229,9 @@ Define a typed method once; neither discovery nor builds execute its handler.
|
|
|
155
229
|
|
|
156
230
|
#### Parameters
|
|
157
231
|
|
|
158
|
-
| Parameter | Type
|
|
159
|
-
| --------- |
|
|
160
|
-
| `method` | `Readonly`\<\{ `description`: `string`; `effect`: `"external"` \| `"read"` \| `"write"`; `requires`: readonly ( \| `"agent.list"` \| `"agent.manage"` \| `"agent.read"` \| `"agent.send"` \| `"computer.list"` \| `"computer.readFile"` \| `"computer.writeFile"` \| `"computer.execute"` \| `"agent.rename"` \| `"agent.toolPolicy"` \| `"apps.open"` \| `"apps.call"` \| `"llm.generation"` \| `"media.generation"` \| `"games:servers"` \| `"git.repository"` \| `"app.storage"` \| `"app.events.emit"` \| `"team.workbenchHost"` \| `"marketplace.browse"` \| `"marketplace.purchase"` \| `"workflow.run"` \| `"workflow.publish"` \| `"workflow.arm"` \| `"world"` \| `"channels.read"` \| `"channels.personal.manage"`)[]; `hidden?`: `true`; `anonymous?`: `true`; `pending?`: `true`; \}
|
|
232
|
+
| Parameter | Type |
|
|
233
|
+
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
234
|
+
| `method` | `Omit`\<`Readonly`\<\{ `description`: `string`; `effect`: `"external"` \| `"read"` \| `"write"`; `requires`: readonly ( \| `"agent.list"` \| `"agent.manage"` \| `"agent.read"` \| `"agent.send"` \| `"computer.list"` \| `"computer.readFile"` \| `"computer.writeFile"` \| `"computer.execute"` \| `"agent.rename"` \| `"agent.avatar"` \| `"agent.toolPolicy"` \| `"approvals"` \| `"apps.open"` \| `"apps.call"` \| `"llm.generation"` \| `"media.generation"` \| `"games:servers"` \| `"git.repository"` \| `"app.storage"` \| `"app.events.emit"` \| `"team.workbenchHost"` \| `"marketplace.browse"` \| `"marketplace.purchase"` \| `"workflow.run"` \| `"workflow.publish"` \| `"workflow.arm"` \| `"world"` \| `"channels.read"` \| `"channels.personal.manage"`)[]; `hidden?`: `true`; `anonymous?`: `true`; `pending?`: `true`; `present?`: `Readonly`\<\{ `verb`: `string`; \}\>; \}\>, `"present"`\> & \{ input: I; output: O; present?: Readonly\<\{ verb: string; row: (call: Readonly\<\{ input: output\<I\>; output: output\<O\>; \}\>) =\> \{ readonly id: string; readonly title: string; ... 4 more ...; readonly media?: \{ ...; \} \| undefined; \}; rows: Readonly\<...\>; \}\> \| undefined; \} & (\{ ...; \} \| \{ ...; \}) |
|
|
161
235
|
|
|
162
236
|
#### Returns
|
|
163
237
|
|