@fungi.computer/tourtois 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +127 -2
- package/dist/AgentMcpServers.d.ts +34 -0
- package/dist/AgentMcpServers.js +124 -0
- package/dist/AppAccessConsent.d.ts +23 -0
- package/dist/AppAccessConsent.js +122 -0
- package/dist/AppCallPermissions.d.ts +9 -0
- package/dist/AppCallPermissions.js +21 -0
- package/dist/AppDesktopSettings.d.ts +11 -0
- package/dist/AppDesktopSettings.js +102 -0
- package/dist/AppPane.d.ts +37 -0
- package/dist/AppPane.js +219 -0
- package/dist/AppWorkbench.d.ts +44 -0
- package/dist/AppWorkbench.js +575 -0
- package/dist/AvatarPicker.d.ts +7 -0
- package/dist/AvatarPicker.js +78 -0
- package/dist/AvatarPreview.d.ts +10 -0
- package/dist/AvatarPreview.js +6 -0
- package/dist/DesktopMenuBar.d.ts +18 -0
- package/dist/DesktopMenuBar.js +23 -0
- package/dist/FocusedAppMenus.d.ts +2 -0
- package/dist/FocusedAppMenus.js +10 -0
- package/dist/Keys.d.ts +131 -0
- package/dist/Keys.js +201 -0
- package/dist/MushroomMark.d.ts +2 -0
- package/dist/MushroomMark.js +5 -0
- package/dist/SettingsHome.d.ts +18 -0
- package/dist/SettingsHome.js +48 -0
- package/dist/SettingsNavigation.d.ts +17 -0
- package/dist/SettingsNavigation.js +153 -0
- package/dist/SystemSettings.d.ts +24 -0
- package/dist/SystemSettings.js +177 -0
- package/dist/TeamDesktop.d.ts +36 -0
- package/dist/TeamDesktop.js +39 -0
- package/dist/WorkbenchAppBar.d.ts +5 -0
- package/dist/WorkbenchAppBar.js +12 -0
- package/dist/app-authority.d.ts +150 -0
- package/dist/app-authority.js +41 -0
- package/dist/app-directory-reconciliation.d.ts +14 -0
- package/dist/app-directory-reconciliation.js +72 -0
- package/dist/app-directory.d.ts +206 -0
- package/dist/app-directory.js +21 -0
- package/dist/app-icon.d.ts +9 -0
- package/dist/app-icon.js +27 -0
- package/dist/app-launch.d.ts +53 -0
- package/dist/app-launch.js +204 -0
- package/dist/app-menus.d.ts +6 -0
- package/dist/app-menus.js +81 -0
- package/dist/app-open-url.d.ts +8 -0
- package/dist/app-open-url.js +63 -0
- package/dist/app-surface.d.ts +62 -0
- package/dist/app-surface.js +10 -0
- package/dist/app-windows.d.ts +134 -0
- package/dist/app-windows.js +21 -0
- package/dist/chrome.css +153 -0
- package/dist/command-dialogs.d.ts +42 -0
- package/dist/command-dialogs.js +194 -0
- package/dist/desktop/Desktop.d.ts +34 -0
- package/dist/desktop/Desktop.js +275 -0
- package/dist/desktop/consent.d.ts +4 -0
- package/dist/desktop/consent.js +64 -0
- package/dist/desktop/gill-frame.d.ts +14 -0
- package/dist/desktop/gill-frame.js +41 -0
- package/dist/desktop/gill-param-picker.d.ts +15 -0
- package/dist/desktop/gill-param-picker.js +145 -0
- package/dist/desktop/launcher-gill.d.ts +20 -0
- package/dist/desktop/launcher-gill.js +49 -0
- package/dist/desktop/own-launchers.d.ts +9 -0
- package/dist/desktop/own-launchers.js +9 -0
- package/dist/desktop/row-gill.d.ts +24 -0
- package/dist/desktop/row-gill.js +136 -0
- package/dist/desktop-api.d.ts +63 -0
- package/dist/desktop-api.js +31 -0
- package/dist/desktop-client.d.ts +19 -0
- package/dist/desktop-client.js +127 -0
- package/dist/desktop-layout.d.ts +117 -0
- package/dist/desktop-layout.js +305 -0
- package/dist/desktop-visibility.d.ts +8 -0
- package/dist/desktop-visibility.js +5 -0
- package/dist/layers.d.ts +5 -0
- package/dist/layers.js +31 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +17 -0
- package/dist/member-avatar-cache.d.ts +15 -0
- package/dist/member-avatar-cache.js +69 -0
- package/dist/member-avatar.d.ts +39 -0
- package/dist/member-avatar.js +88 -0
- package/dist/menu-bar.d.ts +5 -0
- package/dist/menu-bar.js +5 -0
- package/dist/model.d.ts +6 -0
- package/dist/model.js +13 -0
- package/dist/oyster-platform-controls.d.ts +4 -0
- package/dist/oyster-platform-controls.js +107 -0
- package/dist/platform-controls.d.ts +84 -0
- package/dist/platform-controls.js +266 -0
- package/dist/queries.d.ts +7 -0
- package/dist/queries.js +7 -0
- package/dist/session-registry.d.ts +16 -0
- package/dist/session-registry.js +69 -0
- package/dist/session-selection.d.ts +6 -0
- package/dist/session-selection.js +22 -0
- package/dist/settings-leave-guard.d.ts +5 -0
- package/dist/settings-leave-guard.js +9 -0
- package/dist/settings-navigation.css +129 -0
- package/dist/settings-routes.d.ts +118 -0
- package/dist/settings-routes.js +216 -0
- package/dist/settings-sections.d.ts +74 -0
- package/dist/settings-sections.js +17 -0
- package/dist/shell-commands.d.ts +15 -0
- package/dist/shell-commands.js +95 -0
- package/dist/shell-host.d.ts +52 -0
- package/dist/shell-host.js +288 -0
- package/dist/shell-module.d.ts +12 -0
- package/dist/shell-module.js +17 -0
- package/dist/shell-release.d.ts +3 -0
- package/dist/shell-release.js +4 -0
- package/dist/shell.css +858 -0
- package/dist/shell.d.ts +6 -0
- package/dist/shell.js +83 -0
- package/dist/spa.css +11 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +51 -0
- package/dist/team-directory.d.ts +3 -0
- package/dist/team-directory.js +6 -0
- package/dist/use-window-motion.d.ts +3 -0
- package/dist/use-window-motion.js +37 -0
- package/dist/useAppOpenUrlHandoff.d.ts +11 -0
- package/dist/useAppOpenUrlHandoff.js +62 -0
- package/dist/useAppWindowManager.d.ts +351 -0
- package/dist/useAppWindowManager.js +47 -0
- package/dist/useDirectoryCommands.d.ts +12 -0
- package/dist/useDirectoryCommands.js +80 -0
- package/dist/workbench.d.ts +3 -0
- package/dist/workbench.js +2 -0
- package/package.json +222 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { AgentMcpServersPort } from "./AgentMcpServers.js";
|
|
3
|
+
import type { KeysPort } from "./Keys.js";
|
|
4
|
+
import type { MemberAvatarReader, MemberAvatarSave } from "./member-avatar.js";
|
|
5
|
+
import { type SettingsLocation } from "./settings-routes.js";
|
|
6
|
+
export type SettingsCategory = SettingsLocation["category"];
|
|
7
|
+
export type Navigate = (location: SettingsLocation | null) => void;
|
|
8
|
+
export type Accepted = () => Promise<void>;
|
|
9
|
+
export type ConnectorsTarget = Extract<SettingsLocation, {
|
|
10
|
+
category: "connectors";
|
|
11
|
+
}>["target"];
|
|
12
|
+
/** Team administration a host links to from the Settings home. */
|
|
13
|
+
export type TeamAdministrationPort = Readonly<{
|
|
14
|
+
openMembers: () => void;
|
|
15
|
+
/** Present only when the signed-in member may close the Team. */
|
|
16
|
+
close?: Readonly<{
|
|
17
|
+
available: boolean;
|
|
18
|
+
open: () => void;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* The Settings sections an embedding host implements. Tourtois renders a
|
|
23
|
+
* section only when its port is present; an absent port is a section this
|
|
24
|
+
* host does not offer, never a mode flag.
|
|
25
|
+
*/
|
|
26
|
+
export type SettingsSectionPorts = Readonly<{
|
|
27
|
+
/** The person's avatar, read and saved through the host's account. */
|
|
28
|
+
avatar?: Readonly<{
|
|
29
|
+
read: MemberAvatarReader;
|
|
30
|
+
save: MemberAvatarSave;
|
|
31
|
+
}>;
|
|
32
|
+
/** Connectors settings: the host's Agent MCP server custody. */
|
|
33
|
+
agentMcpServers?: AgentMcpServersPort;
|
|
34
|
+
/** A host's own Connectors editor, used when it has no Agent MCP port. */
|
|
35
|
+
connectors?: ComponentType<{
|
|
36
|
+
target: ConnectorsTarget;
|
|
37
|
+
onNavigate: Navigate;
|
|
38
|
+
}>;
|
|
39
|
+
/** Each Computer's outside access. */
|
|
40
|
+
computers?: ComponentType;
|
|
41
|
+
/** Keys settings: the host's owner-direct Keys custody. */
|
|
42
|
+
keys?: KeysPort;
|
|
43
|
+
/** The Team's role defaults. */
|
|
44
|
+
defaults?: ComponentType<{
|
|
45
|
+
onAccepted: Accepted;
|
|
46
|
+
}>;
|
|
47
|
+
/** Channels and website assistants. */
|
|
48
|
+
channels?: ComponentType;
|
|
49
|
+
/** Selling Apps; a host offers it only to members who may sell. */
|
|
50
|
+
selling?: ComponentType<{
|
|
51
|
+
appId: string | null;
|
|
52
|
+
onNavigate: Navigate;
|
|
53
|
+
}>;
|
|
54
|
+
/** An App's Permissions tab. Without it, Apps show their Desktop tab. */
|
|
55
|
+
appPermissions?: ComponentType<{
|
|
56
|
+
selectedKey: string | null;
|
|
57
|
+
onAccepted: Accepted;
|
|
58
|
+
}>;
|
|
59
|
+
/** An App's Advanced tab, kept mounted so its drafts survive tab changes. */
|
|
60
|
+
appAdvanced?: ComponentType<{
|
|
61
|
+
selectedKey: string | null;
|
|
62
|
+
visible: boolean;
|
|
63
|
+
onNavigate: Navigate;
|
|
64
|
+
onAccepted: Accepted;
|
|
65
|
+
}>;
|
|
66
|
+
team?: TeamAdministrationPort;
|
|
67
|
+
/** Counts the host already loaded for the Settings home. Null is not zero. */
|
|
68
|
+
summary?: Readonly<{
|
|
69
|
+
defaultAppCount: number | null;
|
|
70
|
+
computerCount: number | null;
|
|
71
|
+
}>;
|
|
72
|
+
}>;
|
|
73
|
+
/** Categories within the host's ceiling that a port actually backs. */
|
|
74
|
+
export declare function offeredSettings(ceiling: readonly SettingsCategory[] | undefined, ports: SettingsSectionPorts): readonly SettingsCategory[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { settingsLocationSchema, } from "./settings-routes.js";
|
|
2
|
+
const categories = settingsLocationSchema.options.map((option) => option.shape.category.value);
|
|
3
|
+
/** Categories within the host's ceiling that a port actually backs. */
|
|
4
|
+
export function offeredSettings(ceiling, ports) {
|
|
5
|
+
const backed = {
|
|
6
|
+
home: true,
|
|
7
|
+
apps: true,
|
|
8
|
+
avatar: ports.avatar !== undefined,
|
|
9
|
+
connectors: ports.agentMcpServers !== undefined || ports.connectors !== undefined,
|
|
10
|
+
computers: ports.computers !== undefined,
|
|
11
|
+
keys: ports.keys !== undefined,
|
|
12
|
+
defaults: ports.defaults !== undefined,
|
|
13
|
+
channels: ports.channels !== undefined,
|
|
14
|
+
selling: ports.selling !== undefined,
|
|
15
|
+
};
|
|
16
|
+
return categories.filter((category) => backed[category] && (ceiling === undefined || ceiling.includes(category)));
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CommandDialogs, type ShellCommandState } from "./command-dialogs.js";
|
|
2
|
+
import { type ShellActions } from "@fungi.computer/oyster/shell/commands";
|
|
3
|
+
import type { TourtoisAppSurfaceHost } from "./app-surface.js";
|
|
4
|
+
/** The Back hint for this platform, e.g. "Back (Ctrl K, then [)". */
|
|
5
|
+
export declare function backHint(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Register the shell's commands, dispatch their direct chords from the host
|
|
8
|
+
* document, and own the which-key and `:` line open state.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useShellCommands(host: TourtoisAppSurfaceHost | null, actions: Omit<ShellActions, "showKeys" | "showCommandLine" | "back"> & Readonly<{
|
|
11
|
+
back: () => void;
|
|
12
|
+
showDesktop: () => Promise<void>;
|
|
13
|
+
}>): ShellCommandState;
|
|
14
|
+
/** Draw the shared shell dialogs in Tourtois's dialog layer. */
|
|
15
|
+
export declare function ShellCommandDialogs(props: Omit<Parameters<typeof CommandDialogs>[0], "container" | "renderIcon">): React.JSX.Element;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CommandDialogs } from "./command-dialogs.js";
|
|
3
|
+
import { useLayer } from "@fungi.computer/maosaico";
|
|
4
|
+
import { Sigil, isSigilName } from "@fungi.computer/sigils";
|
|
5
|
+
import { backBinding, shellCommands, } from "@fungi.computer/oyster/shell/commands";
|
|
6
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
7
|
+
import { AppIcon } from "./app-icon.js";
|
|
8
|
+
import { installShellKeys } from "@fungi.computer/oyster/shell/keys";
|
|
9
|
+
/** The Back hint for this platform, e.g. "Back (Ctrl K, then [)". */
|
|
10
|
+
export function backHint() {
|
|
11
|
+
const key = backBinding.slice("<leader> ".length);
|
|
12
|
+
return `Back (${isMac() ? "⌘K" : "Ctrl K"}, then ${key})`;
|
|
13
|
+
}
|
|
14
|
+
const emptySnapshot = Object.freeze({
|
|
15
|
+
bindings: Object.freeze([]),
|
|
16
|
+
palette: Object.freeze([]),
|
|
17
|
+
slash: Object.freeze([]),
|
|
18
|
+
help: Object.freeze([]),
|
|
19
|
+
menu: Object.freeze([]),
|
|
20
|
+
agent: Object.freeze([]),
|
|
21
|
+
});
|
|
22
|
+
const noSubscription = () => () => undefined;
|
|
23
|
+
const emptyReader = () => emptySnapshot;
|
|
24
|
+
function isMac() {
|
|
25
|
+
return /Mac|iPhone|iPad/u.test(navigator.userAgent);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Register the shell's commands, dispatch their direct chords from the host
|
|
29
|
+
* document, and own the which-key and `:` line open state.
|
|
30
|
+
*/
|
|
31
|
+
export function useShellCommands(host, actions) {
|
|
32
|
+
const [keysOpen, setKeysOpen] = useState(false);
|
|
33
|
+
const [commandLineOpen, setCommandLineOpen] = useState(false);
|
|
34
|
+
const keysOpenRef = useRef(keysOpen);
|
|
35
|
+
keysOpenRef.current = keysOpen;
|
|
36
|
+
const active = useSyncExternalStore(host?.subscribe ?? noSubscription, host?.active ?? emptyReader, host?.active ?? emptyReader);
|
|
37
|
+
// Commands register once per host; each run reads the latest behavior.
|
|
38
|
+
const latest = useRef(actions);
|
|
39
|
+
latest.current = actions;
|
|
40
|
+
const commands = useMemo(() => [
|
|
41
|
+
...shellCommands({
|
|
42
|
+
openApp: () => {
|
|
43
|
+
setKeysOpen(false);
|
|
44
|
+
setCommandLineOpen(false);
|
|
45
|
+
latest.current.openApp();
|
|
46
|
+
},
|
|
47
|
+
// The leader twice opens the `:` line, like tmux's prefix-prefix.
|
|
48
|
+
showKeys: () => {
|
|
49
|
+
const again = keysOpenRef.current;
|
|
50
|
+
setKeysOpen(!again);
|
|
51
|
+
setCommandLineOpen(again);
|
|
52
|
+
},
|
|
53
|
+
showCommandLine: () => {
|
|
54
|
+
setKeysOpen(false);
|
|
55
|
+
setCommandLineOpen(true);
|
|
56
|
+
},
|
|
57
|
+
focusStep: (step) => {
|
|
58
|
+
latest.current.focusStep(step);
|
|
59
|
+
},
|
|
60
|
+
swapStep: (step) => {
|
|
61
|
+
latest.current.swapStep(step);
|
|
62
|
+
},
|
|
63
|
+
toggleZoom: () => {
|
|
64
|
+
latest.current.toggleZoom();
|
|
65
|
+
},
|
|
66
|
+
back: () => {
|
|
67
|
+
latest.current.back();
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
{
|
|
71
|
+
id: "desktop.show",
|
|
72
|
+
title: "Show desktop",
|
|
73
|
+
category: "Window",
|
|
74
|
+
description: "Hide visible Apps, or restore the Apps hidden by Show desktop.",
|
|
75
|
+
projections: {
|
|
76
|
+
bindings: { values: ["<leader> d"], order: 8 },
|
|
77
|
+
palette: { order: 18 },
|
|
78
|
+
help: { order: 8 },
|
|
79
|
+
},
|
|
80
|
+
handler: async () => {
|
|
81
|
+
setKeysOpen(false);
|
|
82
|
+
setCommandLineOpen(false);
|
|
83
|
+
await latest.current.showDesktop();
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
], []);
|
|
87
|
+
useEffect(() => host?.registerShellCommands?.(commands), [host, commands]);
|
|
88
|
+
useEffect(() => (host ? installShellKeys(document.body, host, isMac()) : undefined), [host]);
|
|
89
|
+
return { active, keysOpen, setKeysOpen, commandLineOpen, setCommandLineOpen };
|
|
90
|
+
}
|
|
91
|
+
/** Draw the shared shell dialogs in Tourtois's dialog layer. */
|
|
92
|
+
export function ShellCommandDialogs(props) {
|
|
93
|
+
const container = useLayer("dialog");
|
|
94
|
+
return (_jsx(CommandDialogs, { ...props, container: container, renderIcon: (roles, sigil) => sigil && isSigilName(sigil) ? (_jsx(Sigil, { name: sigil })) : (_jsx(AppIcon, { roles: roles })) }));
|
|
95
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type TeamWorkbenchHostHandle, type WorkbenchPlatformDestination, type WorkbenchGillRequest, type WorkbenchGillWatch } from "@fungi.computer/oyster/shell/workbench";
|
|
2
|
+
import { type PlatformControls } from "./platform-controls.js";
|
|
3
|
+
import type { GillReply } from "@fungi.computer/gills";
|
|
4
|
+
import type { MemberAvatar } from "@fungi.computer/sprite-zdog/avatar";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
import { type AppOpenInput } from "@fungi.computer/agar/apps";
|
|
7
|
+
import type { TourtoisAppSurfaceHost } from "./app-surface.js";
|
|
8
|
+
import { type WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
|
|
9
|
+
declare const teamIdentitySchema: z.ZodObject<{
|
|
10
|
+
team: z.ZodObject<{
|
|
11
|
+
teamId: z.ZodString;
|
|
12
|
+
name: z.ZodString;
|
|
13
|
+
handle: z.ZodString;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
}, z.core.$strict>;
|
|
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
|
+
export type TeamComputerPage = z.output<typeof workbenchComputerDirectorySchema>;
|
|
25
|
+
/**
|
|
26
|
+
* The trusted shell's view of its host: Team/Computer/App directory reads,
|
|
27
|
+
* child-App mounting and host-offered platform destinations. The host
|
|
28
|
+
* composes Tourtois into its own top-level document, so Tourtois runs with
|
|
29
|
+
* the host's escalated permissions; each child App stays in its own sandboxed
|
|
30
|
+
* frame, mounted inside its window.
|
|
31
|
+
*/
|
|
32
|
+
export type TourtoisShell = Readonly<{
|
|
33
|
+
team(): Promise<TeamIdentity>;
|
|
34
|
+
computers(input: {
|
|
35
|
+
limit: number;
|
|
36
|
+
offset: number;
|
|
37
|
+
}): Promise<TeamComputerPage>;
|
|
38
|
+
directory(): Promise<WorkbenchDirectory>;
|
|
39
|
+
platformDestinations(): Promise<readonly WorkbenchPlatformDestination[]>;
|
|
40
|
+
settingsSections(): Promise<readonly string[] | null>;
|
|
41
|
+
openPlatformDestination(id: string): Promise<void>;
|
|
42
|
+
takeAppOpenRequest(): Promise<AppOpenInput | null>;
|
|
43
|
+
memberAvatar(): Promise<MemberAvatar | null>;
|
|
44
|
+
/** Team administration the host renders with the owner's own authority. */
|
|
45
|
+
platform: PlatformControls;
|
|
46
|
+
/** Host-admitted Gill requests; the App never receives another App's fetch credentials. */
|
|
47
|
+
requestGill(input: WorkbenchGillRequest): Promise<GillReply>;
|
|
48
|
+
watchGillChanges(input: WorkbenchGillRequest, onChanged: () => Promise<void>): Promise<WorkbenchGillWatch>;
|
|
49
|
+
host: TourtoisAppSurfaceHost;
|
|
50
|
+
}>;
|
|
51
|
+
export declare function createTourtoisShell(workbench: TeamWorkbenchHostHandle): TourtoisShell;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
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 { teamComputerDirectorySchema, teamComputerSchema, } from "@fungi.computer/oyster/shell/workbench";
|
|
3
|
+
import { createAppSurfaceCommandHost } from "@fungi.computer/oyster/shell/command-host";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import {} from "./platform-controls.js";
|
|
6
|
+
import { parseMemberAvatar } from "./member-avatar.js";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import { appOpenInputSchema, } from "@fungi.computer/agar/apps";
|
|
9
|
+
import { workbenchDirectorySchema, } from "@fungi.computer/oyster/shell/workbench-directory";
|
|
10
|
+
const teamIdentitySchema = z.strictObject({
|
|
11
|
+
team: z.strictObject({
|
|
12
|
+
teamId: z.string().min(1),
|
|
13
|
+
name: z.string().min(1),
|
|
14
|
+
handle: z.string().min(1),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
const workbenchComputerDirectorySchema = teamComputerDirectorySchema.extend({
|
|
18
|
+
computers: z.array(teamComputerSchema.pick({ computerId: true, name: true }).strip()),
|
|
19
|
+
});
|
|
20
|
+
const platformDestinationsSchema = z
|
|
21
|
+
.array(z.strictObject({
|
|
22
|
+
id: z.string().min(1).max(64),
|
|
23
|
+
label: z.string().min(1).max(80),
|
|
24
|
+
}))
|
|
25
|
+
.max(8);
|
|
26
|
+
const relayId = z.string().min(1).max(64);
|
|
27
|
+
const relayLabel = z.string().min(1).max(APP_NAVIGATION_LABEL_MAX);
|
|
28
|
+
const relayCommands = (maximum) => z
|
|
29
|
+
.array(z.strictObject({
|
|
30
|
+
id: relayId,
|
|
31
|
+
label: relayLabel,
|
|
32
|
+
variant: z.enum(["primary", "outline", "ghost"]).optional(),
|
|
33
|
+
tone: z.literal("destructive").optional(),
|
|
34
|
+
disabled: z.boolean().optional(),
|
|
35
|
+
}))
|
|
36
|
+
.max(maximum);
|
|
37
|
+
const navigationRelaySchema = z
|
|
38
|
+
.strictObject({
|
|
39
|
+
canGoBack: z.boolean(),
|
|
40
|
+
title: z.string().trim().min(1).max(APP_NAVIGATION_TITLE_MAX).optional(),
|
|
41
|
+
crumbs: z
|
|
42
|
+
.array(z.strictObject({
|
|
43
|
+
id: relayId,
|
|
44
|
+
label: relayLabel,
|
|
45
|
+
siblings: z
|
|
46
|
+
.array(z.strictObject({ id: relayId, label: relayLabel }))
|
|
47
|
+
.max(APP_NAVIGATION_SIBLINGS_MAX)
|
|
48
|
+
.optional(),
|
|
49
|
+
}))
|
|
50
|
+
.max(APP_NAVIGATION_CRUMBS_MAX)
|
|
51
|
+
.optional(),
|
|
52
|
+
primary: relayCommands(APP_NAVIGATION_PRIMARY_MAX).optional(),
|
|
53
|
+
menu: relayCommands(APP_NAVIGATION_MENU_MAX).optional(),
|
|
54
|
+
status: z
|
|
55
|
+
.array(z.strictObject({
|
|
56
|
+
id: relayId,
|
|
57
|
+
label: relayLabel,
|
|
58
|
+
tone: z
|
|
59
|
+
.enum(["neutral", "info", "warning", "success", "error"])
|
|
60
|
+
.optional(),
|
|
61
|
+
}))
|
|
62
|
+
.max(APP_NAVIGATION_STATUS_MAX)
|
|
63
|
+
.optional(),
|
|
64
|
+
})
|
|
65
|
+
.nullable();
|
|
66
|
+
/** A child's navigation state as relayed by the host, re-checked here. */
|
|
67
|
+
function parseNavigationRelay(value) {
|
|
68
|
+
const state = navigationRelaySchema.parse(value);
|
|
69
|
+
if (state === null)
|
|
70
|
+
return null;
|
|
71
|
+
let parsed = { canGoBack: state.canGoBack };
|
|
72
|
+
if (state.title !== undefined)
|
|
73
|
+
parsed = { ...parsed, title: state.title };
|
|
74
|
+
if (state.crumbs !== undefined)
|
|
75
|
+
parsed = {
|
|
76
|
+
...parsed,
|
|
77
|
+
crumbs: state.crumbs.map((crumb) => {
|
|
78
|
+
const { siblings, ...rest } = crumb;
|
|
79
|
+
return siblings === undefined ? rest : { ...rest, siblings };
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
if (state.primary !== undefined)
|
|
83
|
+
parsed = { ...parsed, primary: state.primary.map(commandOf) };
|
|
84
|
+
if (state.menu !== undefined)
|
|
85
|
+
parsed = { ...parsed, menu: state.menu.map(commandOf) };
|
|
86
|
+
if (state.status !== undefined)
|
|
87
|
+
parsed = { ...parsed, status: state.status.map(statusOf) };
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
function commandOf(command) {
|
|
91
|
+
let copy = { id: command.id, label: command.label };
|
|
92
|
+
if (command.variant !== undefined)
|
|
93
|
+
copy = { ...copy, variant: command.variant };
|
|
94
|
+
if (command.tone !== undefined)
|
|
95
|
+
copy = { ...copy, tone: command.tone };
|
|
96
|
+
if (command.disabled !== undefined)
|
|
97
|
+
copy = { ...copy, disabled: command.disabled };
|
|
98
|
+
return copy;
|
|
99
|
+
}
|
|
100
|
+
function statusOf(item) {
|
|
101
|
+
const copy = { id: item.id, label: item.label };
|
|
102
|
+
return item.tone === undefined ? copy : { ...copy, tone: item.tone };
|
|
103
|
+
}
|
|
104
|
+
function childLaunch(launch) {
|
|
105
|
+
return {
|
|
106
|
+
windowId: launch.windowId,
|
|
107
|
+
appId: launch.appId,
|
|
108
|
+
releaseId: launch.releaseId,
|
|
109
|
+
surfaceId: launch.surfaceId,
|
|
110
|
+
href: launch.href,
|
|
111
|
+
title: launch.title,
|
|
112
|
+
openRequest: launch.openRequest,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function createTourtoisShell(workbench) {
|
|
116
|
+
// One Whistle graph for Tourtois's own shell commands and every mounted
|
|
117
|
+
// child's Release-validated commands, so the menu bar and Cmd-K follow focus.
|
|
118
|
+
const commands = createAppSurfaceCommandHost();
|
|
119
|
+
const mount = async (container, launch, signal, { onAgentSessionSelection, onComputerChoice, onNavigation, onAppOpen, onAppRole, onSelectionChanged, onNavigationState, onStatusItems, onFocus, }) => {
|
|
120
|
+
const commandMount = commands.mount(launch.surfaceId, launch.commands);
|
|
121
|
+
let resolveHandle = () => undefined;
|
|
122
|
+
const handleReady = new Promise((resolve) => {
|
|
123
|
+
resolveHandle = resolve;
|
|
124
|
+
});
|
|
125
|
+
let contribution;
|
|
126
|
+
const events = {
|
|
127
|
+
...(onComputerChoice && { onComputerChoice }),
|
|
128
|
+
onAgentSessionSelection: (selection) => onAgentSessionSelection(selection),
|
|
129
|
+
...(onNavigation && { onNavigation }),
|
|
130
|
+
...(onAppOpen && { onAppOpen }),
|
|
131
|
+
...(onAppRole && { onAppRole }),
|
|
132
|
+
...(onSelectionChanged && { onSelectionChanged }),
|
|
133
|
+
onCommands: async (presented) => {
|
|
134
|
+
if (presented.length === 0) {
|
|
135
|
+
contribution?.dispose();
|
|
136
|
+
contribution = undefined;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (contribution !== undefined) {
|
|
140
|
+
contribution.update(presented);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
contribution = await commandMount.handlers.registerCommands({
|
|
144
|
+
commands: presented,
|
|
145
|
+
invoke: async (commandId, invocation) => (await handleReady).invokeCommand(commandId, invocation.arguments === undefined
|
|
146
|
+
? { origin: invocation.origin }
|
|
147
|
+
: {
|
|
148
|
+
arguments: invocation.arguments,
|
|
149
|
+
origin: invocation.origin,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
onReservedChord: (input) => commandMount.handlers.invokeReservedChord(input),
|
|
154
|
+
onFocus: (focused) => {
|
|
155
|
+
if (focused) {
|
|
156
|
+
commands.focus(launch.surfaceId);
|
|
157
|
+
onFocus?.();
|
|
158
|
+
}
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
},
|
|
161
|
+
...(onNavigationState && {
|
|
162
|
+
onNavigationState: (state) =>
|
|
163
|
+
// The host parsed this at the App's edge; parse again at ours.
|
|
164
|
+
Promise.resolve(state).then((relayed) => {
|
|
165
|
+
onNavigationState(parseNavigationRelay(relayed));
|
|
166
|
+
}),
|
|
167
|
+
}),
|
|
168
|
+
...(onStatusItems && {
|
|
169
|
+
onStatusItems: (items) =>
|
|
170
|
+
// The host parsed these at the App's edge; parse again at ours.
|
|
171
|
+
Promise.resolve(items).then((relayed) => {
|
|
172
|
+
onStatusItems(parseAppStatusItems(relayed));
|
|
173
|
+
}),
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
let handle;
|
|
177
|
+
try {
|
|
178
|
+
// The child's frame lives in this window's own slot, so the browser
|
|
179
|
+
// moves, clips and stacks it with the window.
|
|
180
|
+
handle = await workbench.mount(container, childLaunch(launch), events);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
commandMount.dispose();
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
resolveHandle(handle);
|
|
187
|
+
if (signal.aborted) {
|
|
188
|
+
commandMount.dispose();
|
|
189
|
+
await handle.dispose();
|
|
190
|
+
throw signal.reason;
|
|
191
|
+
}
|
|
192
|
+
const close = async () => {
|
|
193
|
+
signal.removeEventListener("abort", closeFromParent);
|
|
194
|
+
commandMount.dispose();
|
|
195
|
+
await handle.dispose();
|
|
196
|
+
};
|
|
197
|
+
const closeFromParent = () => {
|
|
198
|
+
void close().catch((cause) => {
|
|
199
|
+
console.error("Tourtois shell operation failed", cause);
|
|
200
|
+
return undefined;
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
signal.addEventListener("abort", closeFromParent, { once: true });
|
|
204
|
+
return {
|
|
205
|
+
setVisibility: (visibility) => handle.setVisibility(visibility),
|
|
206
|
+
reconfigure: (nextLaunch) => handle.reconfigure(childLaunch(nextLaunch)),
|
|
207
|
+
requestClose: () => handle.requestClose(),
|
|
208
|
+
requestBack: () => handle.requestBack(),
|
|
209
|
+
activateNavigation: (activation) => handle.activateNavigation(activation),
|
|
210
|
+
activateStatus: (action) => handle.activateStatus(action),
|
|
211
|
+
dispose: close,
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const host = Object.freeze({
|
|
215
|
+
mount,
|
|
216
|
+
reportAgentSessionSelection: (_teamId, windowId, selection) => workbench.reportAgentSessionSelection(windowId, selection),
|
|
217
|
+
focus: commands.focus,
|
|
218
|
+
registerShellCommands: commands.registerShellCommands,
|
|
219
|
+
registerDirectoryCommands: commands.registerDirectoryCommands,
|
|
220
|
+
menu: commands.menu,
|
|
221
|
+
snapshot: commands.snapshot,
|
|
222
|
+
active: commands.active,
|
|
223
|
+
execute: commands.execute,
|
|
224
|
+
subscribe: commands.subscribe,
|
|
225
|
+
});
|
|
226
|
+
const changeListeners = new Set();
|
|
227
|
+
let watching;
|
|
228
|
+
const platform = {
|
|
229
|
+
registerDesktopSettings: (handler) => workbench.registerDesktopSettings(handler),
|
|
230
|
+
openSettings: (location) => {
|
|
231
|
+
// The host renders Settings itself. Restore our own invoker only after
|
|
232
|
+
// that host-owned dialog closes.
|
|
233
|
+
const active = document.activeElement;
|
|
234
|
+
const invoker = active instanceof HTMLElement &&
|
|
235
|
+
active.closest('[role="menu"]') === null
|
|
236
|
+
? active
|
|
237
|
+
: document.querySelector(".tourtois-system-menu-trigger");
|
|
238
|
+
void workbench
|
|
239
|
+
.openSettings(location)
|
|
240
|
+
.then(() => {
|
|
241
|
+
if (invoker?.isConnected)
|
|
242
|
+
invoker.focus();
|
|
243
|
+
})
|
|
244
|
+
.catch((cause) => {
|
|
245
|
+
console.error("Tourtois shell operation failed", cause);
|
|
246
|
+
return undefined;
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
subscribeChanges: (listener) => {
|
|
250
|
+
changeListeners.add(listener);
|
|
251
|
+
watching ??= workbench
|
|
252
|
+
.watchSettingsChanges(() => {
|
|
253
|
+
for (const notify of changeListeners)
|
|
254
|
+
notify();
|
|
255
|
+
return Promise.resolve();
|
|
256
|
+
})
|
|
257
|
+
.catch((cause) => {
|
|
258
|
+
console.error("Tourtois shell operation failed", cause);
|
|
259
|
+
return undefined;
|
|
260
|
+
});
|
|
261
|
+
return () => {
|
|
262
|
+
changeListeners.delete(listener);
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
team: async () => teamIdentitySchema.parse(await workbench.team()).team,
|
|
268
|
+
computers: async (input) => workbenchComputerDirectorySchema.parse(await workbench.computers(input)),
|
|
269
|
+
directory: async () => workbenchDirectorySchema.parse(await workbench.directory()),
|
|
270
|
+
platformDestinations: async () => platformDestinationsSchema.parse(await workbench.platformDestinations()),
|
|
271
|
+
settingsSections: async () => z
|
|
272
|
+
.array(z.string())
|
|
273
|
+
.nullable()
|
|
274
|
+
.parse(await workbench.settingsSections()),
|
|
275
|
+
openPlatformDestination: (id) => workbench.openPlatformDestination(id),
|
|
276
|
+
takeAppOpenRequest: async () => {
|
|
277
|
+
const request = await workbench.takeAppOpenRequest();
|
|
278
|
+
return request === null
|
|
279
|
+
? null
|
|
280
|
+
: Schema.decodeUnknownSync(appOpenInputSchema)(request);
|
|
281
|
+
},
|
|
282
|
+
memberAvatar: async () => parseMemberAvatar(await workbench.memberAvatar()),
|
|
283
|
+
platform,
|
|
284
|
+
requestGill: (input) => workbench.requestGill(input),
|
|
285
|
+
watchGillChanges: (input, onChanged) => workbench.watchGillChanges(input, onChanged),
|
|
286
|
+
host,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MountPlatformControls, MountShell } from "@fungi.computer/oyster/shell/shell-module";
|
|
2
|
+
/**
|
|
3
|
+
* This Release's shell module, emitted as `shell.js` beside `index.html`.
|
|
4
|
+
* A host imports it from its own origin after first paint; Tourtois's code
|
|
5
|
+
* and styles then load as one lazy chunk under the module's own URL.
|
|
6
|
+
*/
|
|
7
|
+
export declare const mountShell: MountShell;
|
|
8
|
+
/**
|
|
9
|
+
* Tourtois's own Settings for a host that has none, such as a standalone
|
|
10
|
+
* Oyster; they load as their own lazy chunk under this module's URL.
|
|
11
|
+
*/
|
|
12
|
+
export declare const mountPlatformControls: MountPlatformControls;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Release's shell module, emitted as `shell.js` beside `index.html`.
|
|
3
|
+
* A host imports it from its own origin after first paint; Tourtois's code
|
|
4
|
+
* and styles then load as one lazy chunk under the module's own URL.
|
|
5
|
+
*/
|
|
6
|
+
export const mountShell = async (root, input) => {
|
|
7
|
+
const shell = await import("./shell-release.js");
|
|
8
|
+
return shell.mountShell(root, input);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Tourtois's own Settings for a host that has none, such as a standalone
|
|
12
|
+
* Oyster; they load as their own lazy chunk under this module's URL.
|
|
13
|
+
*/
|
|
14
|
+
export const mountPlatformControls = async (root, input) => {
|
|
15
|
+
const settings = await import("./oyster-platform-controls.js");
|
|
16
|
+
return settings.mountPlatformControls(root, input);
|
|
17
|
+
};
|