@fungi.computer/tourtois 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 (54) hide show
  1. package/README.md +19 -0
  2. package/dist/AppAccessConsent.d.ts +6 -1
  3. package/dist/AppAccessConsent.js +4 -5
  4. package/dist/AppDesktopSettings.d.ts +2 -2
  5. package/dist/AppPane.d.ts +11 -4
  6. package/dist/AppPane.js +86 -11
  7. package/dist/AppWorkbench.d.ts +1 -1
  8. package/dist/AppWorkbench.js +10 -5
  9. package/dist/AvatarPreview.d.ts +8 -1
  10. package/dist/AvatarPreview.js +9 -1
  11. package/dist/Keys.d.ts +42 -121
  12. package/dist/Keys.js +200 -157
  13. package/dist/OysterComputers.d.ts +22 -0
  14. package/dist/OysterComputers.js +177 -0
  15. package/dist/SettingsHome.js +4 -4
  16. package/dist/SettingsNavigation.d.ts +1 -1
  17. package/dist/SettingsNavigation.js +1 -1
  18. package/dist/SystemSettings.d.ts +2 -2
  19. package/dist/SystemSettings.js +1 -1
  20. package/dist/TeamDesktop.d.ts +1 -1
  21. package/dist/app-authority.d.ts +47 -47
  22. package/dist/app-authority.js +1 -1
  23. package/dist/app-directory-reconciliation.js +7 -2
  24. package/dist/app-directory.d.ts +6 -5
  25. package/dist/app-directory.js +4 -1
  26. package/dist/app-launch.d.ts +1 -1
  27. package/dist/app-launch.js +6 -3
  28. package/dist/app-menus.d.ts +1 -1
  29. package/dist/app-surface.d.ts +2 -1
  30. package/dist/app-windows.d.ts +32 -2
  31. package/dist/app-windows.js +1 -1
  32. package/dist/command-dialogs.d.ts +1 -1
  33. package/dist/command-dialogs.js +4 -4
  34. package/dist/desktop/Desktop.d.ts +1 -1
  35. package/dist/desktop/consent.d.ts +1 -1
  36. package/dist/desktop/consent.js +1 -1
  37. package/dist/desktop/launcher-gill.js +1 -1
  38. package/dist/desktop/row-gill.d.ts +1 -1
  39. package/dist/oyster-platform-controls.d.ts +1 -1
  40. package/dist/oyster-platform-controls.js +64 -23
  41. package/dist/platform-controls.d.ts +6 -5
  42. package/dist/platform-controls.js +3 -3
  43. package/dist/queries.d.ts +4 -0
  44. package/dist/queries.js +4 -0
  45. package/dist/shell-commands.d.ts +1 -1
  46. package/dist/shell-commands.js +4 -4
  47. package/dist/shell-host.d.ts +7 -9
  48. package/dist/shell-host.js +102 -12
  49. package/dist/shell-module.d.ts +1 -1
  50. package/dist/shell.d.ts +1 -1
  51. package/dist/shell.js +5 -9
  52. package/dist/useAppWindowManager.d.ts +32 -1
  53. package/dist/useAppWindowManager.js +3 -0
  54. package/package.json +40 -19
@@ -7,12 +7,14 @@ import "@fungi.computer/parakeet/styles.css";
7
7
  import "./shell.css";
8
8
  import { AppError } from "@fungi.computer/agar";
9
9
  import { createQueryClient } from "@fungi.computer/agar/query";
10
- import { workbenchDirectorySchema } from "@fungi.computer/oyster/shell/workbench-directory";
10
+ import { workbenchDirectorySchema } from "@fungi.computer/nacre/workbench-directory";
11
11
  import { QueryClientProvider } from "@tanstack/react-query";
12
12
  import * as S from "effect/Schema";
13
+ import { z } from "zod";
13
14
  import { createRoot } from "react-dom/client";
14
- import { agentMcpServersViewSchema, createPlatformControls, keyIssueResultSchema, keyRecordSchema, keysViewSchema, PlatformControlsView, } from "./platform-controls.js";
15
+ import { agentMcpServersViewSchema, createPlatformControls, PlatformControlsView, } from "./platform-controls.js";
15
16
  import { settingsLocationSchema } from "./settings-routes.js";
17
+ import { OysterComputers, createOysterComputersPort, } from "./OysterComputers.js";
16
18
  async function json(path) {
17
19
  const response = await fetch(path, {
18
20
  credentials: "same-origin",
@@ -61,33 +63,72 @@ const agentMcpServers = {
61
63
  },
62
64
  };
63
65
  /** Owner-direct Keys custody; a plaintext secret comes back only once, on issue. */
64
- const keys = {
65
- list: async () => keysViewSchema.parse(await json("/api/keys")),
66
- issue: async (draft) => {
67
- const response = await ownerRequest("/api/keys", {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify(draft),
71
- }, "The key request was refused");
72
- return keyIssueResultSchema.parse(await response.json());
73
- },
74
- revoke: async (keyId, requestId) => {
75
- const response = await ownerRequest("/api/keys/revoke", {
76
- method: "POST",
77
- headers: { "Content-Type": "application/json" },
78
- body: JSON.stringify({ keyId, requestId }),
79
- }, "The key request was refused");
80
- return keyRecordSchema.parse(await response.json());
81
- },
82
- };
66
+ function createOysterKeysPort(oysterKeysWire) {
67
+ const oysterKeysView = z.strictObject({
68
+ keys: z.array(oysterKeysWire.summary),
69
+ });
70
+ const oysterKeyIssue = z.strictObject({
71
+ plaintext: z.string().min(1),
72
+ key: oysterKeysWire.summary,
73
+ });
74
+ const oysterKeyMutation = z.strictObject({ key: oysterKeysWire.summary });
75
+ return {
76
+ scope: "oyster",
77
+ expiry: "optional",
78
+ options: (apps) => {
79
+ const targets = apps.map((app) => ({
80
+ id: app.installationId,
81
+ name: app.displayName,
82
+ }));
83
+ return Promise.resolve([
84
+ { capability: "apps.read", label: "Read Apps", targets },
85
+ {
86
+ capability: "apps.use",
87
+ label: "Use Apps and run Workflows",
88
+ targets,
89
+ },
90
+ { capability: "computer.use", label: "Use the Computer" },
91
+ ]);
92
+ },
93
+ list: async () => oysterKeysView.parse(await json("/api/keys")),
94
+ issue: async (draft) => {
95
+ const response = await ownerRequest("/api/keys", {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify(oysterKeysWire.issue.parse(draft)),
99
+ }, "The key request was refused");
100
+ return oysterKeyIssue.parse(await response.json());
101
+ },
102
+ revoke: async (keyId, requestId) => {
103
+ const response = await ownerRequest("/api/keys/revoke", {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json" },
106
+ body: JSON.stringify(oysterKeysWire.revoke.parse({ keyId, requestId })),
107
+ }, "The key request was refused");
108
+ const result = oysterKeyMutation.parse(await response.json());
109
+ return { key: result.key, notice: "Key revoked.", retry: false };
110
+ },
111
+ };
112
+ }
83
113
  const directory = async () => workbenchDirectorySchema.parse(await json("/api/workbench/directory"));
84
114
  export const mountPlatformControls = (root, input) => {
115
+ const keys = createOysterKeysPort(input.keysWire);
116
+ const requestAppAccess = (target, request, dialog) => input.requestAppAccess(target, request, {
117
+ ask: dialog.ask,
118
+ customize: (installationId) => dialog.customize({
119
+ category: "apps",
120
+ appKey: installationId === null ? null : `installation:${installationId}`,
121
+ section: "desktop",
122
+ }),
123
+ });
85
124
  const controls = createPlatformControls({
86
- sections: ["home", "apps", "connectors", "keys"],
125
+ sections: ["home", "apps", "connectors", "computers", "keys"],
87
126
  });
127
+ const computersPort = createOysterComputersPort(new URL(window.location.origin));
128
+ const computers = () => (_jsx(OysterComputers, { teamId: input.teamId, port: computersPort, onChanged: controls.changed }));
88
129
  const client = createQueryClient();
89
130
  const settings = createRoot(root);
90
- settings.render(_jsx(QueryClientProvider, { client: client, children: _jsx(PlatformControlsView, { teamId: input.teamId, controls: controls, data: { directory, agentMcpServers, keys } }) }));
131
+ settings.render(_jsx(QueryClientProvider, { client: client, children: _jsx(PlatformControlsView, { teamId: input.teamId, controls: controls, data: { directory, agentMcpServers, computers, keys, requestAppAccess } }) }));
91
132
  return Promise.resolve({
92
133
  controls: {
93
134
  ...controls,
@@ -1,5 +1,6 @@
1
- import type { WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
2
- import { type WorkbenchDesktopSettings, type WorkbenchDesktopRegistration } from "@fungi.computer/oyster/shell/workbench";
1
+ import type { WorkbenchHostControls } from "@fungi.computer/nacre/workbench";
2
+ import type { WorkbenchDirectory } from "@fungi.computer/nacre/workbench-directory";
3
+ import { type WorkbenchDesktopSettings, type WorkbenchDesktopRegistration } from "@fungi.computer/nacre/workbench";
3
4
  import type { AppPermissionOutcome, AppPermissionRequest } from "@fungi.computer/agar";
4
5
  import { type AppAccessRequest } from "./AppAccessConsent.js";
5
6
  import { createSettingsRouter, type SettingsLocation } from "./settings-routes.js";
@@ -7,7 +8,7 @@ export { parseSettingsLocation } from "./settings-routes.js";
7
8
  import type { SettingsSectionPorts } from "./settings-sections.js";
8
9
  export type { SettingsSectionPorts, TeamAdministrationPort, } from "./settings-sections.js";
9
10
  export { agentMcpServersViewSchema, type AgentMcpServerDraft, type AgentMcpServersPort, type AgentMcpServersView, } from "./AgentMcpServers.js";
10
- export { keyGrantSchema, keyGrantTargetSchema, keyIssueResultSchema, keyRecordSchema, keysViewSchema, type KeyAppAccessLevel, type KeyAppOption, type KeyGrant, type KeyGrantTarget, type KeyIssueDraft, type KeyIssueResult, type KeyRecord, type KeysPort, type KeysView, } from "./Keys.js";
11
+ export { type KeyAppOption, type KeyGrantOption, type KeyGrantPreviewProps, type KeyTargetOption, type KeyChange, type KeyIssueDraft, type KeyIssueResult, type KeysPort, type KeysView, } from "./Keys.js";
11
12
  /**
12
13
  * Team administration a dashboard may ask for but never performs itself:
13
14
  * System Settings (App permissions, forks, default Apps, Team Git, sprite)
@@ -36,7 +37,7 @@ export type PlatformControlsHost = PlatformControls & Readonly<{
36
37
  desktopConsentReadyAt: () => number;
37
38
  /** A child App can suggest Settings; only the member's Review opens them. */
38
39
  suggestDesktopSettings: (target: AppAccessTarget) => void;
39
- requestAppAccess: (target: AppAccessTarget, input: AppPermissionRequest) => Promise<AppPermissionOutcome>;
40
+ requestAppAccess: WorkbenchHostControls["requestAppAccess"];
40
41
  /** View wiring: current location, navigation and the consent handler. */
41
42
  /** Settings the embedding host backs with real data and actions. */
42
43
  sections?: readonly SettingsLocation["category"][] | undefined;
@@ -74,7 +75,7 @@ export type PlatformControlsData = SettingsSectionPorts & Readonly<{
74
75
  * capabilities; the owner's click in this trusted shell is the only grant
75
76
  * a host without a separate per-App ceiling needs.
76
77
  */
77
- requestAppAccess?(target: AppAccessTarget, input: AppPermissionRequest, dialog: AppAccessDialog): Promise<AppPermissionOutcome>;
78
+ requestAppAccess?(target: Parameters<WorkbenchHostControls["requestAppAccess"]>[0], input: AppPermissionRequest, dialog: AppAccessDialog): Promise<AppPermissionOutcome>;
78
79
  }>;
79
80
  /** Tourtois owns the same Settings renderer on every embedding host. */
80
81
  export declare function PlatformControlsView({ teamId, controls, data, }: {
@@ -2,8 +2,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { AppError } from "@fungi.computer/agar";
3
3
  import { Button } from "@fungi.computer/caps/components/button";
4
4
  import { toast } from "@fungi.computer/caps/components/toaster";
5
- import { desktopGillAdditionSchema, desktopGillSelectionSchema, } from "@fungi.computer/oyster/shell/workbench";
6
- import { addressKey } from "@fungi.computer/oyster/shell/app-directory";
5
+ import { desktopGillAdditionSchema, desktopGillSelectionSchema, } from "@fungi.computer/nacre/workbench";
6
+ import { addressKey } from "@fungi.computer/nacre/app-directory";
7
7
  import { tourtoisQueryKeys } from "./queries.js";
8
8
  import { useQuery } from "@tanstack/react-query";
9
9
  import { lazy, Suspense, useEffect, useState, useSyncExternalStore, } from "react";
@@ -13,7 +13,7 @@ import { createSettingsRouter, navigateSettings, settingsLocationSchema, } from
13
13
  export { parseSettingsLocation } from "./settings-routes.js";
14
14
  import { MemberAvatarSource } from "./member-avatar.js";
15
15
  export { agentMcpServersViewSchema, } from "./AgentMcpServers.js";
16
- export { keyGrantSchema, keyGrantTargetSchema, keyIssueResultSchema, keyRecordSchema, keysViewSchema, } from "./Keys.js";
16
+ export {} from "./Keys.js";
17
17
  /** One host's platform controls; render them with `PlatformControlsView`. */
18
18
  export function createPlatformControls(options) {
19
19
  const sections = options?.sections.slice();
package/dist/queries.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /** Keep host and isolated-workbench reads in distinct cache entries. */
2
2
  export declare const tourtoisQueryKeys: {
3
+ oysterComputers: (teamId: string) => readonly ["tourtois", "oyster-computers", string];
4
+ oysterComputerPage: (teamId: string, offset: number) => readonly ["tourtois", "oyster-computers", string, number];
5
+ oysterSelectedComputer: (teamId: string) => readonly ["tourtois", "oyster-selected-computer", string];
6
+ oysterInternetAccess: (teamId: string, computerId: string) => readonly ["tourtois", "oyster-internet-access", string, string];
3
7
  hostSettings: (workspaceId: string) => readonly ["tourtois", "host-settings", string];
4
8
  workbench: () => readonly ["tourtois", "workbench"];
5
9
  workbenchComputers: (teamId: string) => readonly ["tourtois", "workbench-computers", string];
package/dist/queries.js CHANGED
@@ -1,5 +1,9 @@
1
1
  /** Keep host and isolated-workbench reads in distinct cache entries. */
2
2
  export const tourtoisQueryKeys = {
3
+ oysterComputers: (teamId) => ["tourtois", "oyster-computers", teamId],
4
+ oysterComputerPage: (teamId, offset) => [...tourtoisQueryKeys.oysterComputers(teamId), offset],
5
+ oysterSelectedComputer: (teamId) => ["tourtois", "oyster-selected-computer", teamId],
6
+ oysterInternetAccess: (teamId, computerId) => ["tourtois", "oyster-internet-access", teamId, computerId],
3
7
  hostSettings: (workspaceId) => ["tourtois", "host-settings", workspaceId],
4
8
  workbench: () => ["tourtois", "workbench"],
5
9
  workbenchComputers: (teamId) => ["tourtois", "workbench-computers", teamId],
@@ -1,5 +1,5 @@
1
1
  import { CommandDialogs, type ShellCommandState } from "./command-dialogs.js";
2
- import { type ShellActions } from "@fungi.computer/oyster/shell/commands";
2
+ import { type ShellActions } from "@fungi.computer/nacre/commands";
3
3
  import type { TourtoisAppSurfaceHost } from "./app-surface.js";
4
4
  /** The Back hint for this platform, e.g. "Back (Ctrl K, then [)". */
5
5
  export declare function backHint(): string;
@@ -2,13 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { CommandDialogs } from "./command-dialogs.js";
3
3
  import { useLayer } from "@fungi.computer/maosaico";
4
4
  import { Sigil, isSigilName } from "@fungi.computer/sigils";
5
- import { backBinding, shellCommands, } from "@fungi.computer/oyster/shell/commands";
5
+ import { backBinding, shellCommands, } from "@fungi.computer/nacre/commands";
6
6
  import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
7
7
  import { AppIcon } from "./app-icon.js";
8
- import { installShellKeys } from "@fungi.computer/oyster/shell/keys";
8
+ import { installShellKeys } from "@fungi.computer/nacre/keys";
9
9
  /** The Back hint for this platform, e.g. "Back (Ctrl K, then [)". */
10
10
  export function backHint() {
11
- const key = backBinding.slice("<leader> ".length);
11
+ const key = backBinding.slice("<leader>".length);
12
12
  return `Back (${isMac() ? "⌘K" : "Ctrl K"}, then ${key})`;
13
13
  }
14
14
  const emptySnapshot = Object.freeze({
@@ -73,7 +73,7 @@ export function useShellCommands(host, actions) {
73
73
  category: "Window",
74
74
  description: "Hide visible Apps, or restore the Apps hidden by Show desktop.",
75
75
  projections: {
76
- bindings: { values: ["<leader> d"], order: 8 },
76
+ bindings: { values: ["<leader>d"], order: 8 },
77
77
  palette: { order: 18 },
78
78
  help: { order: 8 },
79
79
  },
@@ -1,11 +1,11 @@
1
- import { type TeamWorkbenchHostHandle, type WorkbenchPlatformDestination, type WorkbenchGillRequest, type WorkbenchGillWatch } from "@fungi.computer/oyster/shell/workbench";
1
+ import { type TeamWorkbenchHostHandle, type WorkbenchPlatformDestination, type WorkbenchGillRequest, type WorkbenchGillWatch } from "@fungi.computer/nacre/workbench";
2
2
  import { type PlatformControls } from "./platform-controls.js";
3
3
  import type { GillReply } from "@fungi.computer/gills";
4
4
  import type { MemberAvatar } from "@fungi.computer/sprite-zdog/avatar";
5
5
  import * as z from "zod";
6
6
  import { type AppOpenInput } from "@fungi.computer/agar/apps";
7
7
  import type { TourtoisAppSurfaceHost } from "./app-surface.js";
8
- import { type WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
8
+ import { workbenchComputerDirectorySchema, type WorkbenchDirectory } from "@fungi.computer/nacre/workbench-directory";
9
9
  declare const teamIdentitySchema: z.ZodObject<{
10
10
  team: z.ZodObject<{
11
11
  teamId: z.ZodString;
@@ -14,14 +14,12 @@ declare const teamIdentitySchema: z.ZodObject<{
14
14
  }, z.core.$strict>;
15
15
  }, z.core.$strict>;
16
16
  export type TeamIdentity = z.output<typeof teamIdentitySchema>["team"];
17
- declare const workbenchComputerDirectorySchema: z.ZodObject<{
18
- nextOffset: z.ZodNullable<z.ZodNumber>;
19
- computers: z.ZodArray<z.ZodObject<{
20
- name: z.ZodString;
21
- computerId: z.ZodString;
22
- }, z.core.$strip>>;
23
- }, z.core.$strict>;
24
17
  export type TeamComputerPage = z.output<typeof workbenchComputerDirectorySchema>;
18
+ /** The complete inventory offered to the shell's Computer picker. */
19
+ export declare function readWorkbenchComputers(workbench: Pick<TourtoisShell, "computers">, signal: AbortSignal): Promise<{
20
+ computerId: string;
21
+ name: string;
22
+ }[]>;
25
23
  /**
26
24
  * The trusted shell's view of its host: Team/Computer/App directory reads,
27
25
  * child-App mounting and host-offered platform destinations. The host
@@ -1,12 +1,12 @@
1
- import { APP_NAVIGATION_TITLE_MAX, parseAppStatusItems, } from "@fungi.computer/agar";
2
- import { teamComputerDirectorySchema, teamComputerSchema, } from "@fungi.computer/oyster/shell/workbench";
3
- import { createAppSurfaceCommandHost } from "@fungi.computer/oyster/shell/command-host";
1
+ import { APP_NAVIGATION_TITLE_MAX, parseAppStatusItems, APP_NAVIGATION_CRUMBS_MAX, APP_NAVIGATION_LABEL_MAX, APP_NAVIGATION_MENU_MAX, APP_NAVIGATION_PRIMARY_MAX, APP_NAVIGATION_SIBLINGS_MAX, APP_NAVIGATION_STATUS_MAX, } from "@fungi.computer/agar";
2
+ import {} from "@fungi.computer/nacre/workbench";
3
+ import { createAppSurfaceCommandHost } from "@fungi.computer/nacre/command-host";
4
4
  import * as Schema from "effect/Schema";
5
5
  import {} from "./platform-controls.js";
6
6
  import { parseMemberAvatar } from "./member-avatar.js";
7
7
  import * as z from "zod";
8
8
  import { appOpenInputSchema, } from "@fungi.computer/agar/apps";
9
- import { workbenchDirectorySchema, } from "@fungi.computer/oyster/shell/workbench-directory";
9
+ import { workbenchDirectorySchema, workbenchComputerDirectorySchema, } from "@fungi.computer/nacre/workbench-directory";
10
10
  const teamIdentitySchema = z.strictObject({
11
11
  team: z.strictObject({
12
12
  teamId: z.string().min(1),
@@ -14,19 +14,72 @@ const teamIdentitySchema = z.strictObject({
14
14
  handle: z.string().min(1),
15
15
  }),
16
16
  });
17
- const workbenchComputerDirectorySchema = teamComputerDirectorySchema.extend({
18
- computers: z.array(teamComputerSchema.pick({ computerId: true, name: true }).strip()),
19
- });
17
+ /** The complete inventory offered to the shell's Computer picker. */
18
+ export async function readWorkbenchComputers(workbench, signal) {
19
+ const computers = [];
20
+ const assertActive = () => {
21
+ if (signal.aborted)
22
+ throw new DOMException("", "AbortError");
23
+ };
24
+ let offset = 0;
25
+ for (;;) {
26
+ assertActive();
27
+ const page = await workbench.computers({ limit: 100, offset });
28
+ assertActive();
29
+ computers.push(...page.computers);
30
+ if (page.nextOffset === null)
31
+ return computers;
32
+ if (page.nextOffset <= offset)
33
+ throw new Error("Computer directory pagination did not advance.");
34
+ offset = page.nextOffset;
35
+ }
36
+ }
20
37
  const platformDestinationsSchema = z
21
38
  .array(z.strictObject({
22
39
  id: z.string().min(1).max(64),
23
40
  label: z.string().min(1).max(80),
24
41
  }))
25
42
  .max(8);
43
+ const relayId = z.string().min(1).max(64);
44
+ const relayLabel = z.string().min(1).max(APP_NAVIGATION_LABEL_MAX);
45
+ const relayCommands = (maximum) => z
46
+ .array(z.strictObject({
47
+ id: relayId,
48
+ label: relayLabel,
49
+ icon: z.literal("play").optional(),
50
+ variant: z.enum(["primary", "outline", "ghost"]).optional(),
51
+ tone: z.literal("destructive").optional(),
52
+ disabled: z.boolean().optional(),
53
+ }))
54
+ .max(maximum);
26
55
  const navigationRelaySchema = z
27
56
  .strictObject({
28
57
  canGoBack: z.boolean(),
29
58
  title: z.string().trim().min(1).max(APP_NAVIGATION_TITLE_MAX).optional(),
59
+ crumbs: z
60
+ .array(z.strictObject({
61
+ id: relayId,
62
+ label: relayLabel,
63
+ siblings: z
64
+ .array(z.strictObject({ id: relayId, label: relayLabel }))
65
+ .max(APP_NAVIGATION_SIBLINGS_MAX)
66
+ .optional(),
67
+ }))
68
+ .max(APP_NAVIGATION_CRUMBS_MAX)
69
+ .optional(),
70
+ primary: relayCommands(APP_NAVIGATION_PRIMARY_MAX).optional(),
71
+ menu: relayCommands(APP_NAVIGATION_MENU_MAX).optional(),
72
+ status: z
73
+ .array(z.strictObject({
74
+ id: relayId,
75
+ label: relayLabel,
76
+ presentation: z.literal("bead").optional(),
77
+ tone: z
78
+ .enum(["neutral", "info", "warning", "success", "error"])
79
+ .optional(),
80
+ }))
81
+ .max(APP_NAVIGATION_STATUS_MAX)
82
+ .optional(),
30
83
  })
31
84
  .nullable();
32
85
  /** A child's navigation state as relayed by the host, re-checked here. */
@@ -34,9 +87,44 @@ function parseNavigationRelay(value) {
34
87
  const state = navigationRelaySchema.parse(value);
35
88
  if (state === null)
36
89
  return null;
37
- return state.title === undefined
38
- ? { canGoBack: state.canGoBack }
39
- : { canGoBack: state.canGoBack, title: state.title };
90
+ let parsed = { canGoBack: state.canGoBack };
91
+ if (state.title !== undefined)
92
+ parsed = { ...parsed, title: state.title };
93
+ if (state.crumbs !== undefined)
94
+ parsed = {
95
+ ...parsed,
96
+ crumbs: state.crumbs.map((crumb) => {
97
+ const { siblings, ...rest } = crumb;
98
+ return siblings === undefined ? rest : { ...rest, siblings };
99
+ }),
100
+ };
101
+ if (state.primary !== undefined)
102
+ parsed = { ...parsed, primary: state.primary.map(commandOf) };
103
+ if (state.menu !== undefined)
104
+ parsed = { ...parsed, menu: state.menu.map(commandOf) };
105
+ if (state.status !== undefined)
106
+ parsed = { ...parsed, status: state.status.map(statusOf) };
107
+ return parsed;
108
+ }
109
+ function commandOf(command) {
110
+ let copy = { id: command.id, label: command.label };
111
+ if (command.icon !== undefined)
112
+ copy = { ...copy, icon: command.icon };
113
+ if (command.variant !== undefined)
114
+ copy = { ...copy, variant: command.variant };
115
+ if (command.tone !== undefined)
116
+ copy = { ...copy, tone: command.tone };
117
+ if (command.disabled !== undefined)
118
+ copy = { ...copy, disabled: command.disabled };
119
+ return copy;
120
+ }
121
+ function statusOf(item) {
122
+ let copy = { id: item.id, label: item.label };
123
+ if (item.presentation !== undefined)
124
+ copy = { ...copy, presentation: item.presentation };
125
+ if (item.tone !== undefined)
126
+ copy = { ...copy, tone: item.tone };
127
+ return copy;
40
128
  }
41
129
  function childLaunch(launch) {
42
130
  return {
@@ -53,7 +141,7 @@ export function createTourtoisShell(workbench) {
53
141
  // One Whistle graph for Tourtois's own shell commands and every mounted
54
142
  // child's Release-validated commands, so the menu bar and Cmd-K follow focus.
55
143
  const commands = createAppSurfaceCommandHost();
56
- const mount = async (container, launch, signal, { onAgentSessionSelection, onComputerChoice, onNavigation, onAppOpen, onAppRole, onSelectionChanged, onNavigationState, onStatusItems, onFocus, }) => {
144
+ const mount = async (container, launch, signal, { onAgentSessionSelection, onDeliveryPending, onComputerChoice, onNavigation, onAppOpen, onAppRole, onSelectionChanged, onNavigationState, onStatusItems, onFocus, }) => {
57
145
  const commandMount = commands.mount(launch.surfaceId, launch.commands);
58
146
  let resolveHandle = () => undefined;
59
147
  const handleReady = new Promise((resolve) => {
@@ -61,6 +149,7 @@ export function createTourtoisShell(workbench) {
61
149
  });
62
150
  let contribution;
63
151
  const events = {
152
+ ...(onDeliveryPending && { onDeliveryPending }),
64
153
  ...(onComputerChoice && { onComputerChoice }),
65
154
  onAgentSessionSelection: (selection) => onAgentSessionSelection(selection),
66
155
  ...(onNavigation && { onNavigation }),
@@ -114,7 +203,7 @@ export function createTourtoisShell(workbench) {
114
203
  try {
115
204
  // The child's frame lives in this window's own slot, so the browser
116
205
  // moves, clips and stacks it with the window.
117
- handle = await workbench.mount(container, childLaunch(launch), events);
206
+ handle = await workbench.mount(container, childLaunch(launch), events, signal);
118
207
  }
119
208
  catch (error) {
120
209
  commandMount.dispose();
@@ -143,6 +232,7 @@ export function createTourtoisShell(workbench) {
143
232
  reconfigure: (nextLaunch) => handle.reconfigure(childLaunch(nextLaunch)),
144
233
  requestClose: () => handle.requestClose(),
145
234
  requestBack: () => handle.requestBack(),
235
+ activateNavigation: (activation) => handle.activateNavigation(activation),
146
236
  activateStatus: (action) => handle.activateStatus(action),
147
237
  dispose: close,
148
238
  };
@@ -1,4 +1,4 @@
1
- import type { MountPlatformControls, MountShell } from "@fungi.computer/oyster/shell/shell-module";
1
+ import type { MountPlatformControls, MountShell } from "@fungi.computer/nacre/shell-module";
2
2
  /**
3
3
  * This Release's shell module, emitted as `shell.js` beside `index.html`.
4
4
  * A host imports it from its own origin after first paint; Tourtois's code
package/dist/shell.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MountShell } from "@fungi.computer/oyster/shell/shell-module";
1
+ import type { MountShell } from "@fungi.computer/nacre/shell-module";
2
2
  /**
3
3
  * Tourtois's shell module contract: render Tourtois as the host's trusted
4
4
  * shell in this top-level document.
package/dist/shell.js CHANGED
@@ -10,7 +10,7 @@ import { QueryClientProvider, useQuery, useQueryClient, } from "@tanstack/react-
10
10
  import { TeamTourtois } from "./TeamDesktop.js";
11
11
  import { AppSurfaceHostProvider } from "./app-surface.js";
12
12
  import { accountAvatarQuery, MemberAvatarSource } from "./member-avatar.js";
13
- import { createTourtoisShell } from "./shell-host.js";
13
+ import { createTourtoisShell, readWorkbenchComputers } from "./shell-host.js";
14
14
  function CanonicalStatus({ message }) {
15
15
  return (_jsx("main", { className: "tourtois-shell tourtois-team-desktop", "data-tourtois-shell": true, children: _jsx("p", { className: "tourtois-directory-status", role: "status", children: message }) }));
16
16
  }
@@ -52,13 +52,7 @@ function MountedTourtois({ shell, }) {
52
52
  return _jsx(CanonicalStatus, { message: "This Team dashboard is unavailable." });
53
53
  if (!connection)
54
54
  return _jsx(CanonicalStatus, { message: "Connecting\u2026" });
55
- return (_jsx(AppSurfaceHostProvider, { value: connection.workbench.host, children: _jsx(MemberAvatarSource, { read: () => connection.workbench.memberAvatar(), children: _jsx(TeamTourtois, { teamId: connection.team.teamId, teamName: connection.team.name, computersReader: (signal) => connection.workbench
56
- .computers({ limit: 100, offset: 0 })
57
- .then((page) => {
58
- if (signal.aborted)
59
- throw new DOMException("", "AbortError");
60
- return page.computers;
61
- }), directoryReader: () => connection.workbench.directory(), takeAppOpenRequest: () => connection.workbench.takeAppOpenRequest(), platform: connection.workbench.platform, desktopStorage: shell.storage, desktopMethods: shell.methods, gills: connection.workbench, settingsLaunchers: settingsLaunchers.filter((launcher) => connection.sections === null ||
55
+ return (_jsx(AppSurfaceHostProvider, { value: connection.workbench.host, children: _jsx(MemberAvatarSource, { read: () => connection.workbench.memberAvatar(), children: _jsx(TeamTourtois, { teamId: connection.team.teamId, teamName: connection.team.name, computersReader: (signal) => readWorkbenchComputers(connection.workbench, signal), directoryReader: () => connection.workbench.directory(), takeAppOpenRequest: () => connection.workbench.takeAppOpenRequest(), platform: connection.workbench.platform, desktopStorage: shell.storage, desktopMethods: shell.methods, gills: connection.workbench, settingsLaunchers: settingsLaunchers.filter((launcher) => connection.sections === null ||
62
56
  connection.sections.includes(launcher.location.category)), systemMenuItems: connection.destinations.map(({ id, label }) => (_jsx(DropdownMenuItem, { onSelect: () => {
63
57
  void connection.workbench
64
58
  .openPlatformDestination(id)
@@ -74,7 +68,9 @@ function MountedTourtois({ shell, }) {
74
68
  */
75
69
  export const mountShell = (root, shell) => {
76
70
  const queryClient = createQueryClient();
77
- const reactRoot = createRoot(root);
71
+ // The Release bundles its own React runtime. Keep its useId values distinct
72
+ // from the embedding host's labels, controls and portalled overlays.
73
+ const reactRoot = createRoot(root, { identifierPrefix: "tourtois-shell-" });
78
74
  reactRoot.render(_jsx(QueryClientProvider, { client: queryClient, children: _jsx(MountedTourtois, { shell: shell }) }));
79
75
  return Promise.resolve(() => {
80
76
  reactRoot.unmount();
@@ -253,6 +253,7 @@ export declare function useAppWindowManager(host: TourtoisAppSurfaceHost | null,
253
253
  }>[];
254
254
  }>) => Promise<void>;
255
255
  back: (windowId: string) => Promise<boolean>;
256
+ activateNavigation: (windowId: string, activation: import("@fungi.computer/agar").AppNavigationActivation) => Promise<boolean>;
256
257
  activateStatus: (windowId: string, action: import("@fungi.computer/agar").AppStatusAction) => Promise<boolean>;
257
258
  registerComputerChoice: (windowId: string, handler: () => void) => () => void;
258
259
  dispose: () => Promise<void>;
@@ -308,13 +309,43 @@ export declare function useAppWindowManager(host: TourtoisAppSurfaceHost | null,
308
309
  }>[];
309
310
  }>;
310
311
  container: HTMLElement;
311
- status: import("@fungi.computer/oyster/shell/window-manager").WindowStatus;
312
+ status: import("@fungi.computer/nacre/window-manager").WindowStatus;
312
313
  generation: number;
313
314
  error?: unknown;
314
315
  cleanupError?: unknown;
315
316
  navigation: Readonly<{
316
317
  canGoBack: boolean;
317
318
  title?: string;
319
+ crumbs?: readonly Readonly<{
320
+ id: string;
321
+ label: string;
322
+ siblings?: readonly Readonly<{
323
+ id: string;
324
+ label: string;
325
+ }>[];
326
+ }>[];
327
+ primary?: readonly Readonly<{
328
+ id: string;
329
+ label: string;
330
+ icon?: "play";
331
+ variant?: "primary" | "outline" | "ghost";
332
+ tone?: "destructive";
333
+ disabled?: boolean;
334
+ }>[];
335
+ menu?: readonly Readonly<{
336
+ id: string;
337
+ label: string;
338
+ icon?: "play";
339
+ variant?: "primary" | "outline" | "ghost";
340
+ tone?: "destructive";
341
+ disabled?: boolean;
342
+ }>[];
343
+ status?: readonly Readonly<{
344
+ id: string;
345
+ label: string;
346
+ presentation?: "bead";
347
+ tone?: "neutral" | "info" | "warning" | "success" | "error";
348
+ }>[];
318
349
  }> | null;
319
350
  statusItems: readonly import("@fungi.computer/agar").AppStatusItem[];
320
351
  }>[];
@@ -10,6 +10,9 @@ export function useAppWindowManager(host, teamId, events) {
10
10
  if (!host)
11
11
  throw new Error("App host unavailable");
12
12
  return await host.mount(container, launch, signal, {
13
+ ...(projectionEvents.onDeliveryPending && {
14
+ onDeliveryPending: projectionEvents.onDeliveryPending,
15
+ }),
13
16
  onAgentSessionSelection: (selection) => {
14
17
  if (!host.reportAgentSessionSelection)
15
18
  return Promise.reject(new Error("Hub Agent browser host unavailable"));