@fungi.computer/agar 0.0.1 → 0.2.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.
Files changed (56) hide show
  1. package/dist/agent-message-meta.d.ts +15 -15
  2. package/dist/agent-message-meta.js +5 -5
  3. package/dist/agents.d.ts +86 -13
  4. package/dist/agents.js +48 -25
  5. package/dist/apps.js +4 -4
  6. package/dist/backend-definition.d.ts +41 -4
  7. package/dist/backend-definition.js +70 -10
  8. package/dist/backend.js +4 -4
  9. package/dist/channels.js +4 -4
  10. package/dist/computer-contracts.d.ts +15 -15
  11. package/dist/computer-contracts.js +14 -14
  12. package/dist/computer.js +16 -16
  13. package/dist/game-servers.js +2 -2
  14. package/dist/git-rpc.d.ts +23 -23
  15. package/dist/git-rpc.js +14 -16
  16. package/dist/git.d.ts +9 -9
  17. package/dist/git.js +8 -8
  18. package/dist/host.d.ts +3 -1
  19. package/dist/host.js +5 -1
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.js +3 -2
  22. package/dist/inference.js +2 -1
  23. package/dist/internal/app-session.js +37 -1
  24. package/dist/internal/protocol.d.ts +4 -4
  25. package/dist/internal/schemas.d.ts +33 -7
  26. package/dist/internal/schemas.js +107 -16
  27. package/dist/marketplace.js +7 -5
  28. package/dist/media-request-id.d.ts +1 -1
  29. package/dist/media-request-id.js +2 -2
  30. package/dist/media.js +2 -2
  31. package/dist/method-presentation.d.ts +7 -0
  32. package/dist/method-presentation.js +14 -0
  33. package/dist/method-refusal.d.ts +5 -5
  34. package/dist/method-refusal.js +4 -4
  35. package/dist/react.d.ts +14 -2
  36. package/dist/react.js +40 -4
  37. package/dist/read-copy.d.ts +1 -1
  38. package/dist/read-copy.js +3 -6
  39. package/dist/storage-wire.d.ts +2 -2
  40. package/dist/storage-wire.js +2 -2
  41. package/dist/storage.js +1 -1
  42. package/dist/test-kit/git.js +19 -2
  43. package/dist/window.d.ts +64 -0
  44. package/dist/window.js +31 -0
  45. package/docs/api/backend-definition.md +83 -9
  46. package/docs/api/backend.md +44 -38
  47. package/docs/api/git-rpc.md +35 -35
  48. package/docs/api/git-wire.md +3 -0
  49. package/docs/api/git.md +66 -80
  50. package/docs/api/index.md +775 -128
  51. package/docs/api/media-request-id.md +1 -1
  52. package/docs/api/method-presentation.md +21 -0
  53. package/docs/api/modules.md +1 -0
  54. package/docs/api/react.md +10 -4
  55. package/docs/api/storage.md +3 -3
  56. package/package.json +10 -8
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ export { createCapabilityRegistry, defineCapability } from "./capabilities.js";
4
4
  export { pending, pendingSchema } from "./pending.js";
5
5
  export type { Pending } from "./pending.js";
6
6
  export type { CapabilityContract, CapabilityDirectory, CapabilityRegistry, } from "./capabilities.js";
7
- export { agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, createAppAgent, renameAppAgent, appAgentCreateInputSchema, appAgentRenameInputSchema, appAgentSchema, appAgentToolPolicyResultSchema, appAgentToolPolicyStatusSchema, listAppAgents, openAppAgent, } from "./agents.js";
8
- export type { AppAgent, AppAgentCreateHandle, AppAgentCreateInput, AppAgentDirectoryHandle, AppAgentListHandle, AppAgentRenameHandle, AppAgentRenameInput, AppAgentToolPolicyHandle, AppAgentToolPolicyResult, AppAgentToolPolicyStatus, AppAgentTransport, AppAgentTransportHandle, } from "./agents.js";
7
+ export { agentAvatar, agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, createAppAgent, renameAppAgent, setAppAgentAvatar, appAgentAvatarInputSchema, appAgentAvatarSchema, appAgentCreateInputSchema, appAgentRenameInputSchema, appAgentSchema, appAgentToolPolicyResultSchema, appAgentToolPolicyStatusSchema, listAppAgents, openAppAgent, } from "./agents.js";
8
+ export type { AppAgent, AppAgentAvatarHandle, AppAgentAvatarInput, AppAgentCreateHandle, AppAgentCreateInput, AppAgentDirectoryHandle, AppAgentListHandle, AppAgentRenameHandle, AppAgentRenameInput, AppAgentToolPolicyHandle, AppAgentToolPolicyResult, AppAgentToolPolicyStatus, AppAgentTransport, AppAgentTransportHandle, } from "./agents.js";
9
9
  export { mediaGeneration, mediaGenerationFailureSchema, mediaGenerationReferenceSchema, mediaGenerationSelectionSchema, mediaModelSchema, openMediaGeneration, } from "./media.js";
10
10
  export type { MediaGenerationHandle, MediaGenerationAttempt, MediaGenerationAttemptHandle, MediaGenerationFailure, MediaGenerationInput, MediaGenerationOutcome, MediaGenerationReference, MediaGenerationResult, MediaGenerationSelection, MediaModel, MediaOutputFormat, MediaGenerationSession, } from "./media.js";
11
11
  export type { AppCommand, AppCommandHandler, AppCommandInvocation, AppCommands, } from "./commands.js";
@@ -25,7 +25,8 @@ export type { AppRoot } from "./root.js";
25
25
  export type { AgentSessionSelection } from "./ui.js";
26
26
  export type { Disposer, ReadonlyStore } from "./store.js";
27
27
  export type { AppBrowserAction, AppBrowserActionOutcome, AppBrowserActionRefusal, AgentAccessOperation, AppPermissionOutcome, AppPermissionRequest, AppStatusAction, AppStatusActionHandler, AppStatusIcon, AppStatusItem, AppStatusOption, AppStatusTone, ComputerAccessOperation, FileNavigationInput, HostUi, ToastInput, } from "./ui.js";
28
- export { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_TITLE_MAX, inertHostNavigation, } from "./window.js";
29
- export type { AppBackHandler, AppNavigationState, AppWindow, BeforeAppClose, HostNavigation, } from "./window.js";
28
+ export { resultRowSchemaV1, type ResultRow } from "@fungi.computer/gills";
29
+ export { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_CRUMBS_MAX, APP_NAVIGATION_LABEL_MAX, APP_NAVIGATION_MENU_MAX, APP_NAVIGATION_PRIMARY_MAX, APP_NAVIGATION_SIBLINGS_MAX, APP_NAVIGATION_STATUS_MAX, APP_NAVIGATION_TITLE_MAX, inertHostNavigation, navigationActivationAllowed, } from "./window.js";
30
+ export type { AppBackHandler, AppCrumb, AppHeaderCommand, AppHeaderStatus, AppNavigationActivation, AppNavigationActivationHandler, AppNavigationState, AppWindow, BeforeAppClose, HostNavigation, } from "./window.js";
30
31
  export { appStorage, AppStorageError } from "./storage.js";
31
32
  export type { AppStorage, AppStorageWatch, AppStorageWrite, AppStorageReset, ResolvedAppStorage, } from "./storage.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { FungiAuthorSchema, FungiDelegationReplySchema, FungiMessageMetaSchema, messageAuthor, messageDelegationReply, authorMeta, } from "./agent-message-meta.js";
2
2
  export { createCapabilityRegistry, defineCapability } from "./capabilities.js";
3
3
  export { pending, pendingSchema } from "./pending.js";
4
- export { agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, createAppAgent, renameAppAgent, appAgentCreateInputSchema, appAgentRenameInputSchema, appAgentSchema, appAgentToolPolicyResultSchema, appAgentToolPolicyStatusSchema, listAppAgents, openAppAgent, } from "./agents.js";
4
+ export { agentAvatar, agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, createAppAgent, renameAppAgent, setAppAgentAvatar, appAgentAvatarInputSchema, appAgentAvatarSchema, appAgentCreateInputSchema, appAgentRenameInputSchema, appAgentSchema, appAgentToolPolicyResultSchema, appAgentToolPolicyStatusSchema, listAppAgents, openAppAgent, } from "./agents.js";
5
5
  export { mediaGeneration, mediaGenerationFailureSchema, mediaGenerationReferenceSchema, mediaGenerationSelectionSchema, mediaModelSchema, openMediaGeneration, } from "./media.js";
6
6
  export { registerCatalog } from "./commands.js";
7
7
  export { connectApp } from "./connect.js";
@@ -11,5 +11,6 @@ export { AppError } from "./errors.js";
11
11
  export { statusActionAllowed, statusText } from "./status.js";
12
12
  export { APP_BROWSER_SAVE_MAX_BYTES, APP_BROWSER_COPY_MAX_BYTES, APP_STATUS_ITEMS_MAX, APP_STATUS_OPTIONS_MAX, APP_STATUS_TEXT_MAX, } from "./ui.js";
13
13
  export { parseStatusAction as parseAppStatusAction, parseStatusItems as parseAppStatusItems, } from "./internal/schemas.js";
14
- export { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_TITLE_MAX, inertHostNavigation, } from "./window.js";
14
+ export { resultRowSchemaV1 } from "@fungi.computer/gills";
15
+ export { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_CRUMBS_MAX, APP_NAVIGATION_LABEL_MAX, APP_NAVIGATION_MENU_MAX, APP_NAVIGATION_PRIMARY_MAX, APP_NAVIGATION_SIBLINGS_MAX, APP_NAVIGATION_STATUS_MAX, APP_NAVIGATION_TITLE_MAX, inertHostNavigation, navigationActivationAllowed, } from "./window.js";
15
16
  export { appStorage, AppStorageError } from "./storage.js";
package/dist/inference.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { defineCapability } from "./capabilities.js";
2
2
  import { rpcFetchHandleSchema, rpcHandleSchema, rpcMethod, } from "./internal/rpc-contract.js";
3
+ import * as z from "zod";
3
4
  import * as Schema from "effect/Schema";
4
5
  import { AppError } from "./errors.js";
5
6
  import { reportCatchFailure } from "./failure.js";
6
- export const llmGeneration = defineCapability("llm.generation", 1, rpcHandleSchema({ open: rpcMethod(rpcFetchHandleSchema) }));
7
+ export const llmGeneration = defineCapability("llm.generation", 1, z.lazy(() => rpcHandleSchema({ open: rpcMethod(rpcFetchHandleSchema) })));
7
8
  const baseURL = "https://inference.fungi.invalid/v1";
8
9
  const decodeResponse = Schema.decodeUnknownSync(Schema.instanceOf(Response));
9
10
  /** OpenAI-compatible transport; no Agent, Session, host URL or credentials are exposed. */
@@ -5,6 +5,7 @@ import * as Schema from "effect/Schema";
5
5
  */
6
6
  import { newMessagePortRpcSession, RpcStub, RpcTarget, } from "@fungi.computer/capnweb";
7
7
  import { statusActionAllowed } from "../status.js";
8
+ import { navigationActivationAllowed } from "../window.js";
8
9
  import { AppError } from "../errors.js";
9
10
  import { reportCatchFailure } from "../failure.js";
10
11
  import { createLocalBindingState, createReservedChordState, installBrowserBridge, } from "./browser-bridge.js";
@@ -13,7 +14,7 @@ import { createCapabilityDirectory, disposeRpcValue, } from "./capabilities.js";
13
14
  import { createAppCommands } from "./commands.js";
14
15
  import { APP_PROTOCOL_VERSION, } from "./protocol.js";
15
16
  import { callRpc, rpcFailure, rpcSuccess, unwrapRpcResult } from "./result.js";
16
- import { parseBrowserAction, parseBrowserActionOutcomeResult, parseBootstrapResult, parseHostReconfiguration, parseContext, parseErrorData, parseFact, parseAgentSessionSelection, parseIdentity, parseLifecycle, parseNavigation, parseStatusAction, parseStatusItems, parseNavigationStateInput, parsePermissionOutcomeResult, parsePermissionRequest, parseReservedChords, parseTheme, parseToast, parseVoidResult, } from "./schemas.js";
17
+ import { parseBrowserAction, parseBrowserActionOutcomeResult, parseBootstrapResult, parseHostReconfiguration, parseContext, parseErrorData, parseFact, parseAgentSessionSelection, parseIdentity, parseLifecycle, parseNavigation, parseNavigationActivation, parseNavigationState, parseStatusAction, parseStatusItems, parseNavigationStateInput, parsePermissionOutcomeResult, parsePermissionRequest, parseReservedChords, parseTheme, parseToast, parseVoidResult, } from "./schemas.js";
17
18
  const rpcStubSchema = Schema.declare((value) => value instanceof RpcStub);
18
19
  import { createReadonlyStore } from "./store.js";
19
20
  class AppClientTarget extends RpcTarget {
@@ -57,6 +58,21 @@ class AppClientTarget extends RpcTarget {
57
58
  return rpcFailure(cause);
58
59
  }
59
60
  }
61
+ async activateNavigation(value) {
62
+ try {
63
+ if (this.#disposed)
64
+ throw disconnected();
65
+ const activation = parseNavigationActivation(value);
66
+ // A shell's stale copy can still send a crumb or command the App has
67
+ // since dropped; only what it published now reaches the App.
68
+ if (!navigationActivationAllowed(this.#state.navigation, activation))
69
+ return rpcSuccess(false);
70
+ return rpcSuccess(await (this.#state.activate?.(activation) ?? false));
71
+ }
72
+ catch (cause) {
73
+ return rpcFailure(cause);
74
+ }
75
+ }
60
76
  async activateStatus(value) {
61
77
  try {
62
78
  if (this.#disposed)
@@ -231,6 +247,7 @@ function createBrokenPromise(remote) {
231
247
  function createClientState() {
232
248
  return {
233
249
  statusItems: [],
250
+ navigation: { canGoBack: false },
234
251
  bindings: createLocalBindingState(),
235
252
  chords: createReservedChordState([]),
236
253
  context: createReadonlyStore({
@@ -293,6 +310,7 @@ function createRootRuntime(options, identity, capabilities) {
293
310
  () => {
294
311
  options.state.beforeClose = undefined;
295
312
  options.state.back = undefined;
313
+ options.state.activate = undefined;
296
314
  options.state.statusAction = undefined;
297
315
  },
298
316
  () => {
@@ -370,8 +388,26 @@ function createAppWindow(state, remote, assertConnected) {
370
388
  assertConnected();
371
389
  const wire = parseNavigationStateInput(input);
372
390
  state.canGoBack = wire.canGoBack;
391
+ state.navigation = parseNavigationState(wire);
373
392
  await callRpc(remote.reportNavigation(wire), parseVoidResult);
374
393
  },
394
+ onActivate(handler) {
395
+ assertConnected();
396
+ if (state.activate !== undefined)
397
+ throw new AppError("The App already owns navigation activations", {
398
+ code: "INVALID_ARGUMENT",
399
+ retryable: false,
400
+ });
401
+ state.activate = handler;
402
+ let released = false;
403
+ return () => {
404
+ if (released)
405
+ return;
406
+ released = true;
407
+ if (state.activate === handler)
408
+ state.activate = undefined;
409
+ };
410
+ },
375
411
  onBack(handler) {
376
412
  assertConnected();
377
413
  if (state.back !== undefined)
@@ -2,7 +2,7 @@ import type { AppCommand } from "../commands.js";
2
2
  import type { WhistleExecutionOrigin, WhistleJsonValue } from "@fungi.computer/whistle";
3
3
  import type { AppContext, AppIdentity, AppLifecycle, FocusState } from "../lifecycle.js";
4
4
  import type { AppBrowserAction, AppBrowserActionOutcome, AgentSessionSelection, AppPermissionOutcome, AppPermissionRequest, AppStatusAction, AppStatusItem, FileNavigationInput, ToastInput } from "../ui.js";
5
- import type { APP_NAVIGATION_CONTRACT_VERSION } from "../window.js";
5
+ import type { APP_NAVIGATION_CONTRACT_VERSION, AppNavigationActivation, AppNavigationState } from "../window.js";
6
6
  import type { AppErrorData } from "./app-error.js";
7
7
  import type { RpcResult } from "./result.js";
8
8
  import type { WhistleFailure } from "@fungi.computer/whistle";
@@ -17,6 +17,8 @@ export type AppClientRpc = {
17
17
  requestClose(): Promise<RpcResult<boolean>>;
18
18
  /** Ask the App to go back one step in its own history; `true` when it did. */
19
19
  requestBack(): Promise<RpcResult<boolean>>;
20
+ /** The person used a crumb or header command; `true` when the App acted. */
21
+ activateNavigation(activation: AppNavigationActivation): Promise<RpcResult<boolean>>;
20
22
  /** The person used one of the App's status items; `true` when it acted. */
21
23
  activateStatus(action: AppStatusAction): Promise<RpcResult<boolean>>;
22
24
  updateContext(context: VersionedFact<AppContext>): Promise<RpcResult<void>>;
@@ -51,9 +53,7 @@ export type AppHostRpc = {
51
53
  /** The versioned navigation contract an App reports to its host. */
52
54
  export type NavigationStateWire = Readonly<{
53
55
  contractVersion: typeof APP_NAVIGATION_CONTRACT_VERSION;
54
- canGoBack: boolean;
55
- title?: string;
56
- }>;
56
+ }> & AppNavigationState;
57
57
  /** The App iframe's handshake message announcing it is ready to connect. */
58
58
  export type AppReadyMessage = {
59
59
  readonly protocolVersion: typeof APP_PROTOCOL_VERSION;
@@ -8,9 +8,9 @@ import type { WhistleJsonValue } from "@fungi.computer/whistle";
8
8
  import type { AppContext, AppIdentity, AppLifecycle, FocusState } from "../lifecycle.js";
9
9
  import type { AppBrowserAction, AppBrowserActionRefusal, AppBrowserActionOutcome, AppPermissionOutcome, AppStatusItem, FileNavigationInput, ToastInput } from "../ui.js";
10
10
  import type { AppErrorData } from "./app-error.js";
11
- import type { CapabilityGrantWire, AppHostReconfigurationWire, CommandInvocationResultWire, HostBootstrapWire, RegisteredCommandWire } from "./protocol.js";
11
+ import type { CapabilityGrantWire, AppHostReconfigurationWire, CommandInvocationResultWire, HostBootstrapWire, NavigationStateWire, RegisteredCommandWire } from "./protocol.js";
12
12
  import type { RpcResult } from "./result.js";
13
- import { APP_NAVIGATION_CONTRACT_VERSION } from "../window.js";
13
+ import { type AppNavigationActivation } from "../window.js";
14
14
  import { type AppConnectMessage, type AppReadyMessage, type CommandInvocationWire, type ConnectRequestWire, type ReservedChord, type VersionedFact } from "./protocol.js";
15
15
  export declare function appInvocation(value: CommandInvocationWire, signal: AbortSignal): Readonly<AppCommandInvocation>;
16
16
  export declare const parseCommand: (input: unknown, options?: import("effect/SchemaAST").ParseOptions | undefined) => Readonly<{
@@ -38,6 +38,7 @@ export declare const parseCommand: (input: unknown, options?: import("effect/Sch
38
38
  } | undefined;
39
39
  readonly slash?: {
40
40
  readonly aliases: readonly string[];
41
+ readonly consumesDraft?: boolean | undefined;
41
42
  readonly order?: number | undefined;
42
43
  readonly arguments?: {
43
44
  readonly hint: string;
@@ -263,13 +264,38 @@ export declare const parseNavigation: (input: unknown, options?: import("effect/
263
264
  export declare const parseNavigationState: (input: unknown, options?: import("effect/SchemaAST").ParseOptions | undefined) => Readonly<{
264
265
  canGoBack: boolean;
265
266
  title?: string;
267
+ crumbs?: readonly Readonly<{
268
+ id: string;
269
+ label: string;
270
+ siblings?: readonly Readonly<{
271
+ id: string;
272
+ label: string;
273
+ }>[];
274
+ }>[];
275
+ primary?: readonly Readonly<{
276
+ id: string;
277
+ label: string;
278
+ variant?: "primary" | "outline" | "ghost";
279
+ tone?: "destructive";
280
+ disabled?: boolean;
281
+ }>[];
282
+ menu?: readonly Readonly<{
283
+ id: string;
284
+ label: string;
285
+ variant?: "primary" | "outline" | "ghost";
286
+ tone?: "destructive";
287
+ disabled?: boolean;
288
+ }>[];
289
+ status?: readonly Readonly<{
290
+ id: string;
291
+ label: string;
292
+ tone?: "neutral" | "info" | "warning" | "success" | "error";
293
+ }>[];
266
294
  }>;
267
295
  /** App edge: trim the title, drop an empty one, then check the wire bounds. */
268
- export declare const parseNavigationStateInput: (input: unknown, options?: import("effect/SchemaAST").ParseOptions | undefined) => Readonly<{
269
- contractVersion: typeof APP_NAVIGATION_CONTRACT_VERSION;
270
- canGoBack: boolean;
271
- title?: string;
272
- }>;
296
+ export declare const parseNavigationStateInput: (input: unknown, options?: import("effect/SchemaAST").ParseOptions | undefined) => NavigationStateWire;
297
+ /** Host to App: one crumb or command the person used. */
298
+ export declare const parseNavigationActivation: (input: unknown, options?: import("effect/SchemaAST").ParseOptions | undefined) => AppNavigationActivation;
273
299
  export declare const parseAgentSessionSelection: (input: unknown, options?: import("effect/SchemaAST").ParseOptions | undefined) => Readonly<{
274
300
  agentId: string;
275
301
  sessionId: string;
@@ -9,7 +9,7 @@ import { flow } from "effect/Function";
9
9
  import { parseCommandDefinition, parseJsonValue, } from "@fungi.computer/whistle/wire";
10
10
  import { isUnsafeStatusCodeUnit } from "../status.js";
11
11
  import { APP_BROWSER_SAVE_MAX_BYTES, APP_BROWSER_COPY_MAX_BYTES, APP_STATUS_ITEMS_MAX, APP_STATUS_OPTIONS_MAX, APP_STATUS_TEXT_MAX, } from "../ui.js";
12
- import { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_TITLE_MAX, } from "../window.js";
12
+ import { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_CRUMBS_MAX, APP_NAVIGATION_LABEL_MAX, APP_NAVIGATION_MENU_MAX, APP_NAVIGATION_PRIMARY_MAX, APP_NAVIGATION_SIBLINGS_MAX, APP_NAVIGATION_STATUS_MAX, APP_NAVIGATION_TITLE_MAX, } from "../window.js";
13
13
  import { AppError } from "../errors.js";
14
14
  import { parseFailure } from "@fungi.computer/whistle/wire";
15
15
  import { APP_CONNECT_TYPE, APP_PROTOCOL_VERSION, APP_READY_TYPE, } from "./protocol.js";
@@ -175,15 +175,62 @@ const navigationSchema = Schema.Struct({
175
175
  const navigationTitleSchema = boundedText(APP_NAVIGATION_TITLE_MAX).pipe(Schema.check(Schema.makeFilter((title) => title.trim() === title, {
176
176
  message: "Expected a trimmed title",
177
177
  })));
178
+ const navigationIdSchema = boundedText(64);
179
+ const navigationLabelSchema = boundedText(APP_NAVIGATION_LABEL_MAX);
180
+ /** Ids name what a person used; repeats would make an activation ambiguous. */
181
+ const uniqueIds = (items) => new Set(items.map((item) => item.id)).size === items.length;
182
+ const crumbSchema = Schema.Struct({
183
+ id: navigationIdSchema,
184
+ label: navigationLabelSchema,
185
+ siblings: Schema.optional(Schema.Array(Schema.Struct({ id: navigationIdSchema, label: navigationLabelSchema })).pipe(Schema.check(Schema.isMaxLength(APP_NAVIGATION_SIBLINGS_MAX)), Schema.check(Schema.makeFilter(uniqueIds, {
186
+ message: "Expected unique crumb sibling ids",
187
+ })))),
188
+ });
189
+ const headerCommandSchema = Schema.Struct({
190
+ id: navigationIdSchema,
191
+ label: navigationLabelSchema,
192
+ variant: Schema.optional(Schema.Literals(["primary", "outline", "ghost"])),
193
+ tone: Schema.optional(Schema.Literal("destructive")),
194
+ disabled: Schema.optional(Schema.Boolean),
195
+ });
196
+ const commandsSchema = (maximum) => Schema.Array(headerCommandSchema).pipe(Schema.check(Schema.isMaxLength(maximum)), Schema.check(Schema.makeFilter(uniqueIds, {
197
+ message: "Expected unique header command ids",
198
+ })));
199
+ const headerStatusSchema = Schema.Struct({
200
+ id: navigationIdSchema,
201
+ label: navigationLabelSchema,
202
+ tone: Schema.optional(Schema.Literals(["neutral", "info", "warning", "success", "error"])),
203
+ });
204
+ const navigationFields = {
205
+ status: Schema.optional(Schema.Array(headerStatusSchema).pipe(Schema.check(Schema.isMaxLength(APP_NAVIGATION_STATUS_MAX)), Schema.check(Schema.makeFilter(uniqueIds, {
206
+ message: "Expected unique header status ids",
207
+ })))),
208
+ crumbs: Schema.optional(Schema.Array(crumbSchema).pipe(Schema.check(Schema.isMaxLength(APP_NAVIGATION_CRUMBS_MAX)), Schema.check(Schema.makeFilter(uniqueIds, { message: "Expected unique crumb ids" })))),
209
+ primary: Schema.optional(commandsSchema(APP_NAVIGATION_PRIMARY_MAX)),
210
+ menu: Schema.optional(commandsSchema(APP_NAVIGATION_MENU_MAX)),
211
+ };
178
212
  const navigationStateWireSchema = Schema.Struct({
179
213
  contractVersion: Schema.Literal(APP_NAVIGATION_CONTRACT_VERSION),
180
214
  canGoBack: Schema.Boolean,
181
215
  title: Schema.optional(navigationTitleSchema),
216
+ ...navigationFields,
182
217
  });
183
218
  const navigationStateInputSchema = Schema.Struct({
184
219
  canGoBack: Schema.Boolean,
185
220
  title: Schema.optional(Schema.String),
221
+ ...navigationFields,
186
222
  });
223
+ const navigationActivationSchema = Schema.Union([
224
+ Schema.Struct({
225
+ kind: Schema.Literal("crumb"),
226
+ crumbId: navigationIdSchema,
227
+ siblingId: Schema.optional(navigationIdSchema),
228
+ }),
229
+ Schema.Struct({
230
+ kind: Schema.Literal("action"),
231
+ actionId: navigationIdSchema,
232
+ }),
233
+ ]);
187
234
  const agentSessionSelectionSchema = Schema.NullOr(Schema.Struct({
188
235
  agentId: boundedText(256),
189
236
  sessionId: boundedText(256),
@@ -339,25 +386,69 @@ export const parseStatusAction = decoder(Schema.Struct({
339
386
  ? { ...action }
340
387
  : { ...action, anchor: frozen({ ...action.anchor }) }), "INVALID_ARGUMENT");
341
388
  export const parseNavigation = decoder(navigationSchema, (value) => frozen({ ...value }), "INVALID_ARGUMENT");
389
+ function statusOf(item) {
390
+ const copy = { id: item.id, label: item.label };
391
+ return frozen(item.tone === undefined ? copy : { ...copy, tone: item.tone });
392
+ }
393
+ function crumbOf(crumb) {
394
+ const copy = { id: crumb.id, label: crumb.label };
395
+ if (crumb.siblings === undefined)
396
+ return frozen(copy);
397
+ return frozen({
398
+ ...copy,
399
+ siblings: frozen(crumb.siblings.map((item) => frozen({ ...item }))),
400
+ });
401
+ }
402
+ function commandOf(command) {
403
+ let copy = { id: command.id, label: command.label };
404
+ if (command.variant !== undefined)
405
+ copy = { ...copy, variant: command.variant };
406
+ if (command.tone !== undefined)
407
+ copy = { ...copy, tone: command.tone };
408
+ if (command.disabled !== undefined)
409
+ copy = { ...copy, disabled: command.disabled };
410
+ return frozen(copy);
411
+ }
412
+ /** One report's fields, each present only when the App sent it. */
413
+ function navigationOf(value) {
414
+ let state = { canGoBack: value.canGoBack };
415
+ if (value.title !== undefined)
416
+ state = { ...state, title: value.title };
417
+ if (value.crumbs !== undefined)
418
+ state = { ...state, crumbs: frozen(value.crumbs.map(crumbOf)) };
419
+ if (value.primary !== undefined)
420
+ state = { ...state, primary: frozen(value.primary.map(commandOf)) };
421
+ if (value.menu !== undefined)
422
+ state = { ...state, menu: frozen(value.menu.map(commandOf)) };
423
+ if (value.status !== undefined)
424
+ state = { ...state, status: frozen(value.status.map(statusOf)) };
425
+ return frozen(state);
426
+ }
342
427
  /** Host edge: one App report, projected without its contract version. */
343
- export const parseNavigationState = decoder(navigationStateWireSchema, (value) => frozen(value.title === undefined
344
- ? { canGoBack: value.canGoBack }
345
- : { canGoBack: value.canGoBack, title: value.title }), "INVALID_ARGUMENT");
428
+ export const parseNavigationState = decoder(navigationStateWireSchema, navigationOf, "INVALID_ARGUMENT");
346
429
  /** App edge: trim the title, drop an empty one, then check the wire bounds. */
347
430
  export const parseNavigationStateInput = decoder(navigationStateInputSchema, (value) => {
348
- const title = value.title?.trim();
349
- return parseNavigationStateWire(title === undefined || title.length === 0
350
- ? {
351
- contractVersion: APP_NAVIGATION_CONTRACT_VERSION,
352
- canGoBack: value.canGoBack,
353
- }
354
- : {
355
- contractVersion: APP_NAVIGATION_CONTRACT_VERSION,
356
- canGoBack: value.canGoBack,
357
- title,
358
- });
431
+ const trimmed = value.title?.trim();
432
+ return parseNavigationStateWire({
433
+ ...navigationOf({
434
+ ...value,
435
+ title: trimmed === undefined || trimmed.length === 0 ? undefined : trimmed,
436
+ }),
437
+ contractVersion: APP_NAVIGATION_CONTRACT_VERSION,
438
+ });
439
+ }, "INVALID_ARGUMENT");
440
+ const parseNavigationStateWire = decoder(navigationStateWireSchema, (value) => frozen({
441
+ ...navigationOf(value),
442
+ contractVersion: APP_NAVIGATION_CONTRACT_VERSION,
443
+ }), "INVALID_ARGUMENT");
444
+ /** Host to App: one crumb or command the person used. */
445
+ export const parseNavigationActivation = decoder(navigationActivationSchema, (value) => {
446
+ if (value.kind === "action")
447
+ return frozen({ ...value });
448
+ return frozen(value.siblingId === undefined
449
+ ? { kind: value.kind, crumbId: value.crumbId }
450
+ : value);
359
451
  }, "INVALID_ARGUMENT");
360
- const parseNavigationStateWire = decoder(navigationStateWireSchema, (value) => frozen({ ...value }), "INVALID_ARGUMENT");
361
452
  export const parseAgentSessionSelection = decoder(agentSessionSelectionSchema, (value) => value === null ? null : frozen({ ...value }), "INVALID_ARGUMENT");
362
453
  export const parsePermissionRequest = decoder(permissionRequestSchema, (value) => frozen({
363
454
  agents: frozen([...new Set(value.agents ?? [])]),
@@ -72,19 +72,21 @@ export const marketplaceAcquisitionSchema = Schema.Union([
72
72
  }),
73
73
  ]);
74
74
  /** Read the Marketplace gallery as the Team that mounted this App. */
75
- export const marketplaceBrowse = defineCapability("marketplace.browse", 1, rpcHandleSchema({
76
- list: rpcMethod(rpcDataSchema(z.unknown().transform((value) => decodeMarketplaceBrowseResult(value)))),
77
- }));
75
+ export const marketplaceBrowse = defineCapability("marketplace.browse", 1, z.lazy(() => rpcHandleSchema({
76
+ list: rpcMethod(rpcDataSchema(z
77
+ .unknown()
78
+ .transform((value) => decodeMarketplaceBrowseResult(value)))),
79
+ })));
78
80
  /**
79
81
  * Acquire a listing for the mounting Team. Free listings install at once;
80
82
  * paid listings return a hosted Checkout URL and install after payment.
81
83
  * The host re-checks Team authority and entitlement on every call.
82
84
  */
83
- export const marketplacePurchase = defineCapability("marketplace.purchase", 1, rpcHandleSchema({
85
+ export const marketplacePurchase = defineCapability("marketplace.purchase", 1, z.lazy(() => rpcHandleSchema({
84
86
  acquire: rpcMethod(rpcDataSchema(z
85
87
  .unknown()
86
88
  .transform((value) => Schema.decodeUnknownSync(marketplaceAcquisitionSchema)(value)))),
87
- }));
89
+ })));
88
90
  /** Anonymous discovery only; acquisition and launch still require Team admission. */
89
91
  export async function browsePublicMarketplace(hubOrigin, category, request = fetch) {
90
92
  const url = new URL("/api/public/marketplace/listings", hubOrigin);
@@ -4,4 +4,4 @@ export declare const mediaRequestIds: Readonly<{
4
4
  readonly request: import("@fungi.computer/cairn/id").IdCodec<"hub-media-request", "request", 1>;
5
5
  }>;
6
6
  /** Public wire grammar for the canonical retry identity, visible in App JSON schemas. */
7
- export declare const mediaRequestIdSchema: z.ZodString;
7
+ export declare const mediaRequestIdSchema: z.ZodLazy<z.ZodString>;
@@ -10,7 +10,7 @@ export const mediaRequestIds = defineIdFamily({
10
10
  },
11
11
  });
12
12
  /** Public wire grammar for the canonical retry identity, visible in App JSON schemas. */
13
- export const mediaRequestIdSchema = z
13
+ export const mediaRequestIdSchema = z.lazy(() => z
14
14
  .string()
15
15
  .length(35)
16
- .regex(/^media_request_[a-z0-9]{21}$/);
16
+ .regex(/^media_request_[a-z0-9]{21}$/));
package/dist/media.js CHANGED
@@ -327,7 +327,7 @@ const mediaAttemptHandleSchema = rpcHandleSchema({
327
327
  /** Capability contract for generating Media (images or video). Apps
328
328
  * declare it to request access; openMediaGeneration uses it to obtain
329
329
  * the host's handle. */
330
- export const mediaGeneration = defineCapability("media.generation", 5, rpcHandleSchema({
330
+ export const mediaGeneration = defineCapability("media.generation", 5, z.lazy(() => rpcHandleSchema({
331
331
  describe: rpcMethod(rpcDataSchema(z.unknown().transform((value) => {
332
332
  const { computerId, canGenerate, models } = Schema.decodeUnknownSync(descriptionSchema, {
333
333
  onExcessProperty: "error",
@@ -337,7 +337,7 @@ export const mediaGeneration = defineCapability("media.generation", 5, rpcHandle
337
337
  createAttempt: rpcMethod(mediaAttemptHandleSchema),
338
338
  restoreAttempt: rpcMethod(mediaAttemptHandleSchema),
339
339
  cancelAttempt: rpcVoid,
340
- }));
340
+ })));
341
341
  /** Parse a value received from the host and turn schema failures into the
342
342
  * transport-safe Agar error used by the other capability helpers. */
343
343
  function decode(schema, value) {
@@ -0,0 +1,7 @@
1
+ import type { ResultRowCodec } from "./backend-definition.js";
2
+ /**
3
+ * The shared result row's decoder and JSON boundary, for
4
+ * `defineAppMethod({ present: { verb, row, rows: resultRows } })`. Only
5
+ * backends that present import this module, so the rest carry no decoder.
6
+ */
7
+ export declare const resultRows: ResultRowCodec;
@@ -0,0 +1,14 @@
1
+ import * as z from "zod";
2
+ import * as Schema from "effect/Schema";
3
+ import { resultRowSchemaV1 } from "@fungi.computer/gills";
4
+ /**
5
+ * The shared result row's decoder and JSON boundary, for
6
+ * `defineAppMethod({ present: { verb, row, rows: resultRows } })`. Only
7
+ * backends that present import this module, so the rest carry no decoder.
8
+ */
9
+ export const resultRows = Object.freeze({
10
+ decode: Schema.decodeUnknownSync(resultRowSchemaV1, {
11
+ onExcessProperty: "error",
12
+ }),
13
+ schema: z.fromJSONSchema(Schema.toJsonSchemaDocument(resultRowSchemaV1).schema),
14
+ });
@@ -29,7 +29,7 @@ export declare class AppMethodRefusal extends Error {
29
29
  constructor(code: AppMethodRefusalCode, message: string);
30
30
  }
31
31
  /** A refused call as it crosses the generated method boundary. */
32
- export declare const appMethodRefusedReplySchema: z.ZodObject<{
32
+ export declare const appMethodRefusedReplySchema: z.ZodLazy<z.ZodObject<{
33
33
  kind: z.ZodLiteral<"refused">;
34
34
  code: z.ZodEnum<{
35
35
  unavailable: "unavailable";
@@ -39,14 +39,14 @@ export declare const appMethodRefusedReplySchema: z.ZodObject<{
39
39
  not_found: "not_found";
40
40
  }>;
41
41
  message: z.ZodString;
42
- }, z.core.$strict>;
42
+ }, z.core.$strict>>;
43
43
  /** A decoded {@link appMethodRefusedReplySchema} value. */
44
44
  export type AppMethodRefusedReply = z.output<typeof appMethodRefusedReplySchema>;
45
45
  /** Every generated App method RPC answers with exactly one of these. */
46
- export declare const appMethodReplySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
46
+ export declare const appMethodReplySchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
47
47
  kind: z.ZodLiteral<"ok">;
48
48
  value: z.ZodJSONSchema;
49
- }, z.core.$strict>, z.ZodObject<{
49
+ }, z.core.$strict>, z.ZodLazy<z.ZodObject<{
50
50
  kind: z.ZodLiteral<"refused">;
51
51
  code: z.ZodEnum<{
52
52
  unavailable: "unavailable";
@@ -56,7 +56,7 @@ export declare const appMethodReplySchema: z.ZodDiscriminatedUnion<[z.ZodObject<
56
56
  not_found: "not_found";
57
57
  }>;
58
58
  message: z.ZodString;
59
- }, z.core.$strict>], "kind">;
59
+ }, z.core.$strict>>], "kind">>;
60
60
  /** A decoded {@link appMethodReplySchema} value; the result is read-only JSON. */
61
61
  export type AppMethodReply = Readonly<{
62
62
  kind: "ok";
@@ -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/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
- }, [onBack]);
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
- report(navigation, title === undefined ? { canGoBack } : { canGoBack, title });
102
- }, [navigation, canGoBack, title]);
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) => {