@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.
- package/README.md +19 -0
- package/dist/AppAccessConsent.d.ts +6 -1
- package/dist/AppAccessConsent.js +4 -5
- package/dist/AppDesktopSettings.d.ts +2 -2
- package/dist/AppPane.d.ts +11 -4
- package/dist/AppPane.js +86 -11
- package/dist/AppWorkbench.d.ts +1 -1
- package/dist/AppWorkbench.js +10 -5
- package/dist/AvatarPreview.d.ts +8 -1
- package/dist/AvatarPreview.js +9 -1
- package/dist/Keys.d.ts +42 -121
- package/dist/Keys.js +200 -157
- package/dist/OysterComputers.d.ts +22 -0
- package/dist/OysterComputers.js +177 -0
- package/dist/SettingsHome.js +4 -4
- package/dist/SettingsNavigation.d.ts +1 -1
- package/dist/SettingsNavigation.js +1 -1
- package/dist/SystemSettings.d.ts +2 -2
- package/dist/SystemSettings.js +1 -1
- package/dist/TeamDesktop.d.ts +1 -1
- package/dist/app-authority.d.ts +47 -47
- package/dist/app-authority.js +1 -1
- package/dist/app-directory-reconciliation.js +7 -2
- package/dist/app-directory.d.ts +6 -5
- package/dist/app-directory.js +4 -1
- package/dist/app-launch.d.ts +1 -1
- package/dist/app-launch.js +6 -3
- package/dist/app-menus.d.ts +1 -1
- package/dist/app-surface.d.ts +2 -1
- package/dist/app-windows.d.ts +32 -2
- package/dist/app-windows.js +1 -1
- package/dist/command-dialogs.d.ts +1 -1
- package/dist/command-dialogs.js +4 -4
- package/dist/desktop/Desktop.d.ts +1 -1
- package/dist/desktop/consent.d.ts +1 -1
- package/dist/desktop/consent.js +1 -1
- package/dist/desktop/launcher-gill.js +1 -1
- package/dist/desktop/row-gill.d.ts +1 -1
- package/dist/oyster-platform-controls.d.ts +1 -1
- package/dist/oyster-platform-controls.js +64 -23
- package/dist/platform-controls.d.ts +6 -5
- package/dist/platform-controls.js +3 -3
- package/dist/queries.d.ts +4 -0
- package/dist/queries.js +4 -0
- package/dist/shell-commands.d.ts +1 -1
- package/dist/shell-commands.js +4 -4
- package/dist/shell-host.d.ts +7 -9
- package/dist/shell-host.js +102 -12
- package/dist/shell-module.d.ts +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +5 -9
- package/dist/useAppWindowManager.d.ts +32 -1
- package/dist/useAppWindowManager.js +3 -0
- 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/
|
|
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,
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 {
|
|
2
|
-
import
|
|
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 {
|
|
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:
|
|
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:
|
|
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/
|
|
6
|
-
import { addressKey } from "@fungi.computer/
|
|
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 {
|
|
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],
|
package/dist/shell-commands.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CommandDialogs, type ShellCommandState } from "./command-dialogs.js";
|
|
2
|
-
import { type ShellActions } from "@fungi.computer/
|
|
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;
|
package/dist/shell-commands.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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>
|
|
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>
|
|
76
|
+
bindings: { values: ["<leader>d"], order: 8 },
|
|
77
77
|
palette: { order: 18 },
|
|
78
78
|
help: { order: 8 },
|
|
79
79
|
},
|
package/dist/shell-host.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type TeamWorkbenchHostHandle, type WorkbenchPlatformDestination, type WorkbenchGillRequest, type WorkbenchGillWatch } from "@fungi.computer/
|
|
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/
|
|
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
|
package/dist/shell-host.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { APP_NAVIGATION_TITLE_MAX, parseAppStatusItems, } from "@fungi.computer/agar";
|
|
2
|
-
import {
|
|
3
|
-
import { createAppSurfaceCommandHost } from "@fungi.computer/
|
|
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/
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
};
|
package/dist/shell-module.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MountPlatformControls, MountShell } from "@fungi.computer/
|
|
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
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
|
-
|
|
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/
|
|
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"));
|