@fungi.computer/tourtois 0.0.0-stage → 0.0.1
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 +184 -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 +107 -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 +224 -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 +323 -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,14 @@
|
|
|
1
|
+
import type { AppWindowManager, AppLaunchDescriptor } from "./app-windows.js";
|
|
2
|
+
export type AppWindowReconciliationTrigger = Readonly<{
|
|
3
|
+
kind: "background-directory-update";
|
|
4
|
+
}> | Readonly<{
|
|
5
|
+
kind: "selection-changed";
|
|
6
|
+
}>;
|
|
7
|
+
/** Replace open windows when the owner-issued App directory changes authority. */
|
|
8
|
+
export declare function reconcileAppDirectory<A extends {
|
|
9
|
+
surfaceId: string;
|
|
10
|
+
}>(appWindows: AppWindowManager, apps: readonly A[], launchFor: (app: A, currentLaunch?: AppLaunchDescriptor) => AppLaunchDescriptor | null, onFailure: (action: "close" | "replace") => void): Promise<void>;
|
|
11
|
+
/** Refresh one stale mount and report whether its old generation was retired. */
|
|
12
|
+
export declare function reconcileAppWindowFromDirectory<A extends {
|
|
13
|
+
surfaceId: string;
|
|
14
|
+
}>(appWindows: AppWindowManager, staleLaunch: AppLaunchDescriptor, apps: readonly A[], launchFor: (app: A, currentLaunch?: AppLaunchDescriptor) => AppLaunchDescriptor | null, trigger: AppWindowReconciliationTrigger, onFailure: (action: "close" | "replace") => void): Promise<boolean>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { sameOfferedComputers } from "./app-launch.js";
|
|
2
|
+
/** Replace open windows when the owner-issued App directory changes authority. */
|
|
3
|
+
export async function reconcileAppDirectory(appWindows, apps, launchFor, onFailure) {
|
|
4
|
+
await Promise.all(appWindows.read().windows.map(async (open) => {
|
|
5
|
+
await reconcileAppWindowFromDirectory(appWindows, open.launch, apps, launchFor, { kind: "background-directory-update" }, onFailure);
|
|
6
|
+
}));
|
|
7
|
+
}
|
|
8
|
+
/** Refresh one stale mount and report whether its old generation was retired. */
|
|
9
|
+
export async function reconcileAppWindowFromDirectory(appWindows, staleLaunch, apps, launchFor, trigger, onFailure) {
|
|
10
|
+
const current = appWindows
|
|
11
|
+
.read()
|
|
12
|
+
.windows.find((window) => window.windowId === staleLaunch.windowId);
|
|
13
|
+
if (!current?.launch ||
|
|
14
|
+
appLaunchRequiresReplacement(staleLaunch, current.launch))
|
|
15
|
+
return true;
|
|
16
|
+
const app = apps.find((candidate) => candidate.surfaceId === staleLaunch.surfaceId);
|
|
17
|
+
const next = app ? launchFor(app, current.launch) : null;
|
|
18
|
+
if (next &&
|
|
19
|
+
!(trigger.kind === "selection-changed"
|
|
20
|
+
? appLaunchRequiresReplacement(current.launch, next)
|
|
21
|
+
: appLaunchRequiresDirectoryUpdate(current.launch, next))) {
|
|
22
|
+
// The same mount keeps its iframe; only the Computers its status bar
|
|
23
|
+
// offers follow the Team's current grants and names. An unreadable
|
|
24
|
+
// inventory offers nothing new, so the window keeps its last offer.
|
|
25
|
+
const offered = next.computers;
|
|
26
|
+
if (offered !== undefined && !sameOfferedComputers(current.launch, next))
|
|
27
|
+
// Only the offer changes, applied to the launch committed when the
|
|
28
|
+
// queue reaches it: a Computer switch queued ahead of it stands.
|
|
29
|
+
await appWindows
|
|
30
|
+
.update(current.windowId, (latest) => ({
|
|
31
|
+
...latest,
|
|
32
|
+
computers: offered,
|
|
33
|
+
}))
|
|
34
|
+
.catch((cause) => {
|
|
35
|
+
console.error("App Computer offer refresh failed", cause);
|
|
36
|
+
});
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const generation = current.generation;
|
|
40
|
+
try {
|
|
41
|
+
if (next)
|
|
42
|
+
await appWindows.replace(current.windowId, next);
|
|
43
|
+
else
|
|
44
|
+
await appWindows.close(current.windowId);
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
console.error("App directory reconciliation failed", cause);
|
|
48
|
+
onFailure(next ? "replace" : "close");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const after = appWindows
|
|
52
|
+
.read()
|
|
53
|
+
.windows.find((window) => window.windowId === current.windowId);
|
|
54
|
+
return after?.generation !== generation;
|
|
55
|
+
}
|
|
56
|
+
function appLaunchRequiresReplacement(current, next) {
|
|
57
|
+
return (next.appId !== current.appId ||
|
|
58
|
+
next.surfaceId !== current.surfaceId ||
|
|
59
|
+
next.releaseId !== current.releaseId ||
|
|
60
|
+
stableLaunchHref(next.href) !== stableLaunchHref(current.href));
|
|
61
|
+
}
|
|
62
|
+
function appLaunchRequiresDirectoryUpdate(current, next) {
|
|
63
|
+
return (next.appId !== current.appId ||
|
|
64
|
+
next.surfaceId !== current.surfaceId ||
|
|
65
|
+
stableLaunchHref(next.href, true) !== stableLaunchHref(current.href, true));
|
|
66
|
+
}
|
|
67
|
+
function stableLaunchHref(href, ignoreReleaseId = false) {
|
|
68
|
+
const url = new URL(href, "https://tourtois.invalid");
|
|
69
|
+
if (ignoreReleaseId)
|
|
70
|
+
url.searchParams.delete("releaseId");
|
|
71
|
+
return url.pathname + url.search;
|
|
72
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { type WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
|
|
2
|
+
/** Shell composition keeps an unread directory distinct from an empty one. */
|
|
3
|
+
export declare function useAppDirectory(teamId: string, directoryReader: () => Promise<WorkbenchDirectory>): {
|
|
4
|
+
query: import("@tanstack/react-query").UseQueryResult<{
|
|
5
|
+
apps: {
|
|
6
|
+
address: {
|
|
7
|
+
kind: "default";
|
|
8
|
+
appId: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "installation";
|
|
11
|
+
installationId: string;
|
|
12
|
+
};
|
|
13
|
+
installationId: string | null;
|
|
14
|
+
surfaceId: string;
|
|
15
|
+
appId: string;
|
|
16
|
+
releaseId: string | null;
|
|
17
|
+
authorityVersion: number;
|
|
18
|
+
enforcement: {
|
|
19
|
+
requestId: string;
|
|
20
|
+
address: {
|
|
21
|
+
kind: "default";
|
|
22
|
+
appId: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "installation";
|
|
25
|
+
installationId: string;
|
|
26
|
+
};
|
|
27
|
+
authorityVersion: number;
|
|
28
|
+
state: "unaffected" | "pending" | "applied" | "expired";
|
|
29
|
+
outstanding: number;
|
|
30
|
+
} | null;
|
|
31
|
+
handlers: "file.open"[];
|
|
32
|
+
roles: string[];
|
|
33
|
+
declaredCapabilities: string[];
|
|
34
|
+
commands: Readonly<{
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
category: string;
|
|
38
|
+
description?: string | undefined;
|
|
39
|
+
sigil?: string | undefined;
|
|
40
|
+
projections?: Readonly<{
|
|
41
|
+
bindings?: (Readonly<{
|
|
42
|
+
order?: number | undefined;
|
|
43
|
+
}> & Readonly<{
|
|
44
|
+
values: readonly string[];
|
|
45
|
+
}>) | undefined;
|
|
46
|
+
agent?: (Readonly<{
|
|
47
|
+
order?: number | undefined;
|
|
48
|
+
}> & Readonly<{
|
|
49
|
+
keybind?: string | undefined;
|
|
50
|
+
}>) | undefined;
|
|
51
|
+
help?: Readonly<{
|
|
52
|
+
order?: number | undefined;
|
|
53
|
+
}> | undefined;
|
|
54
|
+
menu?: Readonly<{
|
|
55
|
+
order?: number | undefined;
|
|
56
|
+
}> | undefined;
|
|
57
|
+
palette?: Readonly<{
|
|
58
|
+
order?: number | undefined;
|
|
59
|
+
}> | undefined;
|
|
60
|
+
slash?: (Readonly<{
|
|
61
|
+
order?: number | undefined;
|
|
62
|
+
}> & Readonly<{
|
|
63
|
+
aliases: readonly string[];
|
|
64
|
+
}>) | undefined;
|
|
65
|
+
}> | undefined;
|
|
66
|
+
}>[];
|
|
67
|
+
gills: import("@fungi.computer/oyster/shell/app-directory").AppDirectoryGill[];
|
|
68
|
+
availability: "available" | "unavailable";
|
|
69
|
+
surface: {
|
|
70
|
+
stableRoute: string;
|
|
71
|
+
} | null;
|
|
72
|
+
surfacePath: string;
|
|
73
|
+
displayName?: string | undefined;
|
|
74
|
+
icon?: string | null | undefined;
|
|
75
|
+
calls?: readonly {
|
|
76
|
+
readonly appId: string;
|
|
77
|
+
readonly methods: readonly string[];
|
|
78
|
+
}[] | undefined;
|
|
79
|
+
computer?: {
|
|
80
|
+
computerId: string;
|
|
81
|
+
name: string;
|
|
82
|
+
} | undefined;
|
|
83
|
+
computerPermission?: {
|
|
84
|
+
kind: "selected-computers";
|
|
85
|
+
computers: {
|
|
86
|
+
computerId: string;
|
|
87
|
+
operations: ("computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute")[];
|
|
88
|
+
}[];
|
|
89
|
+
} | {
|
|
90
|
+
kind: "all-team-computers";
|
|
91
|
+
operations: ("computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute")[];
|
|
92
|
+
} | null | undefined;
|
|
93
|
+
}[];
|
|
94
|
+
roles: Record<string, {
|
|
95
|
+
surfaceId: string | null;
|
|
96
|
+
chosen: boolean;
|
|
97
|
+
}>;
|
|
98
|
+
computer?: {
|
|
99
|
+
computerId: string;
|
|
100
|
+
name: string;
|
|
101
|
+
} | undefined;
|
|
102
|
+
needsUpdate?: {
|
|
103
|
+
installationId: string | null;
|
|
104
|
+
appId: string | null;
|
|
105
|
+
displayName: string;
|
|
106
|
+
}[] | undefined;
|
|
107
|
+
}, Error>;
|
|
108
|
+
apps: {
|
|
109
|
+
address: {
|
|
110
|
+
kind: "default";
|
|
111
|
+
appId: string;
|
|
112
|
+
} | {
|
|
113
|
+
kind: "installation";
|
|
114
|
+
installationId: string;
|
|
115
|
+
};
|
|
116
|
+
installationId: string | null;
|
|
117
|
+
surfaceId: string;
|
|
118
|
+
appId: string;
|
|
119
|
+
releaseId: string | null;
|
|
120
|
+
authorityVersion: number;
|
|
121
|
+
enforcement: {
|
|
122
|
+
requestId: string;
|
|
123
|
+
address: {
|
|
124
|
+
kind: "default";
|
|
125
|
+
appId: string;
|
|
126
|
+
} | {
|
|
127
|
+
kind: "installation";
|
|
128
|
+
installationId: string;
|
|
129
|
+
};
|
|
130
|
+
authorityVersion: number;
|
|
131
|
+
state: "unaffected" | "pending" | "applied" | "expired";
|
|
132
|
+
outstanding: number;
|
|
133
|
+
} | null;
|
|
134
|
+
handlers: "file.open"[];
|
|
135
|
+
roles: string[];
|
|
136
|
+
declaredCapabilities: string[];
|
|
137
|
+
commands: Readonly<{
|
|
138
|
+
id: string;
|
|
139
|
+
title: string;
|
|
140
|
+
category: string;
|
|
141
|
+
description?: string | undefined;
|
|
142
|
+
sigil?: string | undefined;
|
|
143
|
+
projections?: Readonly<{
|
|
144
|
+
bindings?: (Readonly<{
|
|
145
|
+
order?: number | undefined;
|
|
146
|
+
}> & Readonly<{
|
|
147
|
+
values: readonly string[];
|
|
148
|
+
}>) | undefined;
|
|
149
|
+
agent?: (Readonly<{
|
|
150
|
+
order?: number | undefined;
|
|
151
|
+
}> & Readonly<{
|
|
152
|
+
keybind?: string | undefined;
|
|
153
|
+
}>) | undefined;
|
|
154
|
+
help?: Readonly<{
|
|
155
|
+
order?: number | undefined;
|
|
156
|
+
}> | undefined;
|
|
157
|
+
menu?: Readonly<{
|
|
158
|
+
order?: number | undefined;
|
|
159
|
+
}> | undefined;
|
|
160
|
+
palette?: Readonly<{
|
|
161
|
+
order?: number | undefined;
|
|
162
|
+
}> | undefined;
|
|
163
|
+
slash?: (Readonly<{
|
|
164
|
+
order?: number | undefined;
|
|
165
|
+
}> & Readonly<{
|
|
166
|
+
aliases: readonly string[];
|
|
167
|
+
}>) | undefined;
|
|
168
|
+
}> | undefined;
|
|
169
|
+
}>[];
|
|
170
|
+
gills: import("@fungi.computer/oyster/shell/app-directory").AppDirectoryGill[];
|
|
171
|
+
availability: "available" | "unavailable";
|
|
172
|
+
surface: {
|
|
173
|
+
stableRoute: string;
|
|
174
|
+
} | null;
|
|
175
|
+
surfacePath: string;
|
|
176
|
+
displayName?: string | undefined;
|
|
177
|
+
icon?: string | null | undefined;
|
|
178
|
+
calls?: readonly {
|
|
179
|
+
readonly appId: string;
|
|
180
|
+
readonly methods: readonly string[];
|
|
181
|
+
}[] | undefined;
|
|
182
|
+
computer?: {
|
|
183
|
+
computerId: string;
|
|
184
|
+
name: string;
|
|
185
|
+
} | undefined;
|
|
186
|
+
computerPermission?: {
|
|
187
|
+
kind: "selected-computers";
|
|
188
|
+
computers: {
|
|
189
|
+
computerId: string;
|
|
190
|
+
operations: ("computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute")[];
|
|
191
|
+
}[];
|
|
192
|
+
} | {
|
|
193
|
+
kind: "all-team-computers";
|
|
194
|
+
operations: ("computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute")[];
|
|
195
|
+
} | null | undefined;
|
|
196
|
+
}[] | undefined;
|
|
197
|
+
needsUpdate: {
|
|
198
|
+
installationId: string | null;
|
|
199
|
+
appId: string | null;
|
|
200
|
+
displayName: string;
|
|
201
|
+
}[];
|
|
202
|
+
roles: Record<string, {
|
|
203
|
+
surfaceId: string | null;
|
|
204
|
+
chosen: boolean;
|
|
205
|
+
}>;
|
|
206
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { tourtoisQueryKeys } from "./queries.js";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { workbenchDirectorySchema, } from "@fungi.computer/oyster/shell/workbench-directory";
|
|
4
|
+
const noRoles = {};
|
|
5
|
+
/** Shell composition keeps an unread directory distinct from an empty one. */
|
|
6
|
+
export function useAppDirectory(teamId, directoryReader) {
|
|
7
|
+
const query = useQuery({
|
|
8
|
+
queryKey: tourtoisQueryKeys.appDirectory(teamId),
|
|
9
|
+
queryFn: async () => workbenchDirectorySchema.parse(await directoryReader()),
|
|
10
|
+
retry: false,
|
|
11
|
+
// The host's selection read can finish before Tourtois mounts. Keep the
|
|
12
|
+
// result fresh for that handoff; explicit mutations still invalidate it.
|
|
13
|
+
staleTime: 5_000,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
query,
|
|
17
|
+
apps: query.data?.apps,
|
|
18
|
+
needsUpdate: query.data?.needsUpdate ?? [],
|
|
19
|
+
roles: query.data?.roles ?? noRoles,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The App's role Sigil, then its declared icon when the role has no mark. */
|
|
2
|
+
export declare function AppIcon({ roles, icon, }: {
|
|
3
|
+
roles: readonly string[];
|
|
4
|
+
icon?: string | null | undefined;
|
|
5
|
+
}): React.JSX.Element;
|
|
6
|
+
/** An App keeps its declared mark, or a neutral placeholder. */
|
|
7
|
+
export declare function DeclaredAppIcon({ icon, }: {
|
|
8
|
+
icon?: string | null | undefined;
|
|
9
|
+
}): React.JSX.Element;
|
package/dist/app-icon.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Monogram } from "@fungi.computer/caps/components/monogram";
|
|
3
|
+
import { isSigilName, Sigil } from "@fungi.computer/sigils";
|
|
4
|
+
/**
|
|
5
|
+
* Shell marks follow a declared role when one is known; roleless Apps use their
|
|
6
|
+
* own declared Sigil name.
|
|
7
|
+
*/
|
|
8
|
+
const roleSigils = new Map([
|
|
9
|
+
["sessions", "chat"],
|
|
10
|
+
["inbox", "chat"],
|
|
11
|
+
["files", "inkcap"],
|
|
12
|
+
["media", "prints"],
|
|
13
|
+
["terminal", "computer"],
|
|
14
|
+
]);
|
|
15
|
+
/** The App's role Sigil, then its declared icon when the role has no mark. */
|
|
16
|
+
export function AppIcon({ roles, icon, }) {
|
|
17
|
+
const sigil = roles
|
|
18
|
+
.map((role) => roleSigils.get(role))
|
|
19
|
+
.find((name) => name !== undefined);
|
|
20
|
+
if (sigil)
|
|
21
|
+
return _jsx(Sigil, { name: sigil });
|
|
22
|
+
return _jsx(DeclaredAppIcon, { icon: icon });
|
|
23
|
+
}
|
|
24
|
+
/** An App keeps its declared mark, or a neutral placeholder. */
|
|
25
|
+
export function DeclaredAppIcon({ icon, }) {
|
|
26
|
+
return icon && isSigilName(icon) ? (_jsx(Sigil, { name: icon })) : (_jsx(Monogram, { name: "?", subject: "app" }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AppOpenInput } from "@fungi.computer/agar/apps";
|
|
2
|
+
import type { WhistleSnapshot } from "@fungi.computer/whistle";
|
|
3
|
+
import { type TourtoisAppSurfaceHost } from "./app-surface.js";
|
|
4
|
+
import { createAppWindowManager, type AppLaunchDescriptor, type AppWindowManager, type AppWindowProjection } from "./app-windows.js";
|
|
5
|
+
import type { WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
|
|
6
|
+
export type AppDirectoryApp = WorkbenchDirectory["apps"][number];
|
|
7
|
+
export type AppDirectoryRoles = WorkbenchDirectory["roles"];
|
|
8
|
+
/** The Team's effective App for a role when it can be launched now. */
|
|
9
|
+
export declare function appForRole(apps: readonly AppDirectoryApp[], roles: AppDirectoryRoles, role: string): AppDirectoryApp | undefined;
|
|
10
|
+
/** Project a role intent through the Team's current default, including forks. */
|
|
11
|
+
export declare function launchForRoleIntent(apps: readonly AppDirectoryApp[], roles: AppDirectoryRoles, input: AppOpenInput, requestId: string, launchFor: (app: AppDirectoryApp) => AppLaunchDescriptor | null): AppLaunchDescriptor;
|
|
12
|
+
export declare const composeOpenRequests: Readonly<{
|
|
13
|
+
readonly request: import("@fungi.computer/cairn/id").IdCodec<"tourtois-compose-open-request", "request", 1>;
|
|
14
|
+
}>;
|
|
15
|
+
export type ComputerOption = Readonly<{
|
|
16
|
+
computerId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** Build the owner-issued launch descriptor for one currently available surface. */
|
|
20
|
+
export declare function createAppLaunchDescriptor(installation: AppDirectoryApp, homeComputerId: string | null, origin: string, computers: readonly ComputerOption[], computersUnavailable?: boolean): AppLaunchDescriptor | null;
|
|
21
|
+
/**
|
|
22
|
+
* Attach one window to the Computer the person chose in its status bar,
|
|
23
|
+
* from the window's offered list. Only that window changes; a stale or
|
|
24
|
+
* unoffered choice changes nothing.
|
|
25
|
+
* Resolves `false` when the window already uses that Computer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function attachWindowComputer(appWindows: Pick<AppWindowManager, "read" | "update">, windowId: string, computerId: string): Promise<boolean>;
|
|
28
|
+
/** Whether two launches offer the same Computers, in the same order. */
|
|
29
|
+
export declare function sameOfferedComputers(left: AppLaunchDescriptor, right: AppLaunchDescriptor): boolean;
|
|
30
|
+
export declare function launchComputerId(href: string): string | null;
|
|
31
|
+
/** Keep the admitted App identity and point its existing surface at one Computer. */
|
|
32
|
+
export declare function launchForComputer(launch: AppLaunchDescriptor, computerId: string): AppLaunchDescriptor;
|
|
33
|
+
/** Find the user's already-open editor by its admitted open-file command. */
|
|
34
|
+
export declare function findOpenFileEditor<Window extends Pick<AppWindowProjection, "launch">>(windows: readonly Window[], snapshot: WhistleSnapshot): Window | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Select the Team's Files App for file navigation before it is mounted, or
|
|
37
|
+
* any App that declares it opens files when no Files App handles them.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findFileNavigationApp(apps: readonly AppDirectoryApp[], roles: AppDirectoryRoles): AppDirectoryApp | undefined;
|
|
40
|
+
export declare function resolveFileEditorWindow(options: {
|
|
41
|
+
apps: readonly AppDirectoryApp[];
|
|
42
|
+
roles: AppDirectoryRoles;
|
|
43
|
+
appWindows: ReturnType<typeof createAppWindowManager>;
|
|
44
|
+
appSurfaceHost: TourtoisAppSurfaceHost;
|
|
45
|
+
computerId: string;
|
|
46
|
+
computers: readonly ComputerOption[];
|
|
47
|
+
origin: string;
|
|
48
|
+
onPermissionNeeded: (app: AppDirectoryApp) => void;
|
|
49
|
+
}): Promise<Readonly<{
|
|
50
|
+
launch: AppLaunchDescriptor;
|
|
51
|
+
opened: boolean;
|
|
52
|
+
}>>;
|
|
53
|
+
export declare function requireHandledNavigation(outcome: Awaited<ReturnType<TourtoisAppSurfaceHost["execute"]>>): void;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { defineIdFamily } from "@fungi.computer/cairn/id";
|
|
2
|
+
import {} from "./app-surface.js";
|
|
3
|
+
import { AppError } from "@fungi.computer/agar";
|
|
4
|
+
import { createAppWindowManager, } from "./app-windows.js";
|
|
5
|
+
/** The Team's effective App for a role when it can be launched now. */
|
|
6
|
+
export function appForRole(apps, roles, role) {
|
|
7
|
+
const surfaceId = roles[role]?.surfaceId;
|
|
8
|
+
return surfaceId == null
|
|
9
|
+
? undefined
|
|
10
|
+
: apps.find((app) => app.surfaceId === surfaceId &&
|
|
11
|
+
app.availability === "available" &&
|
|
12
|
+
app.releaseId !== null &&
|
|
13
|
+
app.surface !== null);
|
|
14
|
+
}
|
|
15
|
+
/** Project a role intent through the Team's current default, including forks. */
|
|
16
|
+
export function launchForRoleIntent(apps, roles, input, requestId, launchFor) {
|
|
17
|
+
const recipient = "appId" in input
|
|
18
|
+
? apps.find((app) => app.appId === input.appId &&
|
|
19
|
+
app.availability === "available" &&
|
|
20
|
+
app.releaseId !== null &&
|
|
21
|
+
app.surface !== null)
|
|
22
|
+
: appForRole(apps, roles, input.role);
|
|
23
|
+
if (!recipient)
|
|
24
|
+
throw new Error("The requested App is not available in this Team.");
|
|
25
|
+
const launch = launchFor(recipient);
|
|
26
|
+
if (!launch)
|
|
27
|
+
throw new Error("The chosen App is unavailable.");
|
|
28
|
+
return {
|
|
29
|
+
...launch,
|
|
30
|
+
openRequest: { ...input.intent, requestId },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export const composeOpenRequests = defineIdFamily({
|
|
34
|
+
family: "tourtois-compose-open-request",
|
|
35
|
+
version: 1,
|
|
36
|
+
kinds: { request: { prefix: "request" } },
|
|
37
|
+
});
|
|
38
|
+
/** Build the owner-issued launch descriptor for one currently available surface. */
|
|
39
|
+
export function createAppLaunchDescriptor(installation, homeComputerId, origin, computers, computersUnavailable = false) {
|
|
40
|
+
if (installation.availability !== "available" ||
|
|
41
|
+
installation.surface === null ||
|
|
42
|
+
installation.releaseId === null)
|
|
43
|
+
return null;
|
|
44
|
+
const href = new URL(installation.surfacePath, origin);
|
|
45
|
+
href.searchParams.set("teamHasComputers", computersUnavailable ? "unknown" : String(computers.length > 0));
|
|
46
|
+
const allowedComputers = computersForApp(installation, computers);
|
|
47
|
+
const attachesToComputer = installation.computer !== undefined ||
|
|
48
|
+
installation.declaredCapabilities.some((capability) => capability.startsWith("computer."));
|
|
49
|
+
const selectedComputer = allowedComputers.find((computer) => computer.computerId === homeComputerId) ?? allowedComputers[0];
|
|
50
|
+
if (selectedComputer)
|
|
51
|
+
href.searchParams.set("computerId", selectedComputer.computerId);
|
|
52
|
+
href.searchParams.set("authorityVersion", String(installation.authorityVersion));
|
|
53
|
+
href.searchParams.set("releaseId", installation.releaseId);
|
|
54
|
+
href.searchParams.set("surfaceRoute", installation.surface.stableRoute);
|
|
55
|
+
return {
|
|
56
|
+
windowId: "app:" + installation.surfaceId,
|
|
57
|
+
surfaceId: installation.surfaceId,
|
|
58
|
+
installationId: installation.installationId,
|
|
59
|
+
appId: installation.appId,
|
|
60
|
+
releaseId: installation.releaseId,
|
|
61
|
+
title: installation.displayName ?? "App",
|
|
62
|
+
href: href.pathname + href.search,
|
|
63
|
+
openRequest: null,
|
|
64
|
+
roles: installation.roles,
|
|
65
|
+
commands: installation.commands,
|
|
66
|
+
...(attachesToComputer &&
|
|
67
|
+
!computersUnavailable && {
|
|
68
|
+
computers: allowedComputers.map(({ computerId, name }) => ({
|
|
69
|
+
computerId,
|
|
70
|
+
name,
|
|
71
|
+
})),
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Attach one window to the Computer the person chose in its status bar,
|
|
77
|
+
* from the window's offered list. Only that window changes; a stale or
|
|
78
|
+
* unoffered choice changes nothing.
|
|
79
|
+
* Resolves `false` when the window already uses that Computer.
|
|
80
|
+
*/
|
|
81
|
+
export async function attachWindowComputer(appWindows, windowId, computerId) {
|
|
82
|
+
const open = appWindows
|
|
83
|
+
.read()
|
|
84
|
+
.windows.find((item) => item.windowId === windowId);
|
|
85
|
+
if (!open?.launch.computers?.some((computer) => computer.computerId === computerId))
|
|
86
|
+
throw new AppError("That Computer is not offered to this App", {
|
|
87
|
+
code: "CAPABILITY_DENIED",
|
|
88
|
+
retryable: false,
|
|
89
|
+
});
|
|
90
|
+
if (launchComputerId(open.launch.href) === computerId)
|
|
91
|
+
return false;
|
|
92
|
+
// Derive from the launch committed when the window's queue reaches this
|
|
93
|
+
// change, so a concurrent offer refresh is neither lost nor reverted.
|
|
94
|
+
await appWindows.update(windowId, (latest) => {
|
|
95
|
+
if (!latest.computers?.some((computer) => computer.computerId === computerId))
|
|
96
|
+
throw new AppError("That Computer is no longer offered to this App", {
|
|
97
|
+
code: "CAPABILITY_DENIED",
|
|
98
|
+
retryable: false,
|
|
99
|
+
});
|
|
100
|
+
return launchForComputer(latest, computerId);
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/** Whether two launches offer the same Computers, in the same order. */
|
|
105
|
+
export function sameOfferedComputers(left, right) {
|
|
106
|
+
if (left.computers === undefined || right.computers === undefined)
|
|
107
|
+
return left.computers === right.computers;
|
|
108
|
+
const next = right.computers;
|
|
109
|
+
return (left.computers.length === next.length &&
|
|
110
|
+
left.computers.every((computer, index) => {
|
|
111
|
+
const other = next.at(index);
|
|
112
|
+
return (computer.computerId === other?.computerId &&
|
|
113
|
+
computer.name === other.name);
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
function computersForApp(app, computers) {
|
|
117
|
+
// A standalone host supplies its fixed context, not fabricated Team grants.
|
|
118
|
+
// Only an offered inventory entry can become a window's Computer choice.
|
|
119
|
+
if (app.computer !== undefined)
|
|
120
|
+
return computers.filter((computer) => computer.computerId === app.computer?.computerId);
|
|
121
|
+
const permission = app.computerPermission;
|
|
122
|
+
if (!permission)
|
|
123
|
+
return [];
|
|
124
|
+
if (permission.kind === "all-team-computers" &&
|
|
125
|
+
permission.operations.length > 0)
|
|
126
|
+
return computers;
|
|
127
|
+
if (permission.kind !== "selected-computers")
|
|
128
|
+
return [];
|
|
129
|
+
const allowedIds = new Set(permission.computers
|
|
130
|
+
.filter((computer) => computer.operations.length > 0)
|
|
131
|
+
.map((computer) => computer.computerId));
|
|
132
|
+
return computers.filter((computer) => allowedIds.has(computer.computerId));
|
|
133
|
+
}
|
|
134
|
+
export function launchComputerId(href) {
|
|
135
|
+
return new URL(href, "https://tourtois.invalid").searchParams.get("computerId");
|
|
136
|
+
}
|
|
137
|
+
/** Keep the admitted App identity and point its existing surface at one Computer. */
|
|
138
|
+
export function launchForComputer(launch, computerId) {
|
|
139
|
+
const href = new URL(launch.href, "https://tourtois.invalid");
|
|
140
|
+
href.searchParams.set("computerId", computerId);
|
|
141
|
+
return {
|
|
142
|
+
...launch,
|
|
143
|
+
href: href.pathname + href.search,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Find the user's already-open editor by its admitted open-file command. */
|
|
147
|
+
export function findOpenFileEditor(windows, snapshot) {
|
|
148
|
+
return windows.find((window) => snapshot.menu.some((command) => command.commandId === `${window.launch.surfaceId}:open-file`));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Select the Team's Files App for file navigation before it is mounted, or
|
|
152
|
+
* any App that declares it opens files when no Files App handles them.
|
|
153
|
+
*/
|
|
154
|
+
export function findFileNavigationApp(apps, roles) {
|
|
155
|
+
const files = appForRole(apps, roles, "files");
|
|
156
|
+
if (files?.handlers.includes("file.open"))
|
|
157
|
+
return files;
|
|
158
|
+
return apps.find((app) => app.availability === "available" &&
|
|
159
|
+
app.releaseId !== null &&
|
|
160
|
+
app.surface !== null &&
|
|
161
|
+
app.handlers.includes("file.open"));
|
|
162
|
+
}
|
|
163
|
+
export async function resolveFileEditorWindow(options) {
|
|
164
|
+
const existing = findOpenFileEditor(options.appWindows.read().windows, options.appSurfaceHost.snapshot());
|
|
165
|
+
const installation = existing
|
|
166
|
+
? options.apps.find((app) => app.surfaceId === existing.launch.surfaceId)
|
|
167
|
+
: findFileNavigationApp(options.apps, options.roles);
|
|
168
|
+
if (!installation)
|
|
169
|
+
throw new Error("No file editor is available for this Team.");
|
|
170
|
+
if (!computersForApp(installation, options.computers).some((computer) => computer.computerId === options.computerId)) {
|
|
171
|
+
options.onPermissionNeeded(installation);
|
|
172
|
+
throw new Error("The file editor needs permission to use this Computer.");
|
|
173
|
+
}
|
|
174
|
+
if (existing)
|
|
175
|
+
return { launch: existing.launch, opened: false };
|
|
176
|
+
const candidate = createAppLaunchDescriptor(installation, options.computerId, options.origin, options.computers);
|
|
177
|
+
if (!candidate)
|
|
178
|
+
throw new Error("No file editor is available for this Team.");
|
|
179
|
+
await options.appWindows.open(candidate);
|
|
180
|
+
const mounted = options.appWindows
|
|
181
|
+
.read()
|
|
182
|
+
.windows.find((window) => window.windowId === candidate.windowId);
|
|
183
|
+
const editor = mounted
|
|
184
|
+
? findOpenFileEditor([mounted], options.appSurfaceHost.snapshot())
|
|
185
|
+
: undefined;
|
|
186
|
+
if (editor)
|
|
187
|
+
return { launch: editor.launch, opened: true };
|
|
188
|
+
await options.appWindows.close(candidate.windowId).catch((cause) => {
|
|
189
|
+
console.error("App workbench operation failed", cause);
|
|
190
|
+
return undefined;
|
|
191
|
+
});
|
|
192
|
+
throw new Error("The configured file editor is unavailable.");
|
|
193
|
+
}
|
|
194
|
+
export function requireHandledNavigation(outcome) {
|
|
195
|
+
if (outcome.status === "handled")
|
|
196
|
+
return;
|
|
197
|
+
if (outcome.status === "failed")
|
|
198
|
+
throw new Error(outcome.error.message);
|
|
199
|
+
if (outcome.status === "unavailable")
|
|
200
|
+
throw new Error(outcome.reason);
|
|
201
|
+
if (outcome.status === "closed")
|
|
202
|
+
throw new Error("The editor has closed.");
|
|
203
|
+
throw new Error("The editor's open-file command is unavailable.");
|
|
204
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AppSurfaceCommandHost } from "@fungi.computer/oyster/shell/command-host";
|
|
2
|
+
/** Render the focused App's Whistle commands in the desktop's system bar. */
|
|
3
|
+
export declare function AppMenus({ host, container: popover, }: Readonly<{
|
|
4
|
+
host: Pick<AppSurfaceCommandHost, "subscribe" | "menu" | "execute"> | null;
|
|
5
|
+
container: HTMLElement | null;
|
|
6
|
+
}>): React.JSX.Element | null;
|