@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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -2
  3. package/dist/AgentMcpServers.d.ts +34 -0
  4. package/dist/AgentMcpServers.js +124 -0
  5. package/dist/AppAccessConsent.d.ts +23 -0
  6. package/dist/AppAccessConsent.js +122 -0
  7. package/dist/AppCallPermissions.d.ts +9 -0
  8. package/dist/AppCallPermissions.js +21 -0
  9. package/dist/AppDesktopSettings.d.ts +11 -0
  10. package/dist/AppDesktopSettings.js +102 -0
  11. package/dist/AppPane.d.ts +37 -0
  12. package/dist/AppPane.js +184 -0
  13. package/dist/AppWorkbench.d.ts +44 -0
  14. package/dist/AppWorkbench.js +575 -0
  15. package/dist/AvatarPicker.d.ts +7 -0
  16. package/dist/AvatarPicker.js +78 -0
  17. package/dist/AvatarPreview.d.ts +10 -0
  18. package/dist/AvatarPreview.js +6 -0
  19. package/dist/DesktopMenuBar.d.ts +18 -0
  20. package/dist/DesktopMenuBar.js +23 -0
  21. package/dist/FocusedAppMenus.d.ts +2 -0
  22. package/dist/FocusedAppMenus.js +10 -0
  23. package/dist/Keys.d.ts +131 -0
  24. package/dist/Keys.js +201 -0
  25. package/dist/MushroomMark.d.ts +2 -0
  26. package/dist/MushroomMark.js +5 -0
  27. package/dist/SettingsHome.d.ts +18 -0
  28. package/dist/SettingsHome.js +48 -0
  29. package/dist/SettingsNavigation.d.ts +17 -0
  30. package/dist/SettingsNavigation.js +153 -0
  31. package/dist/SystemSettings.d.ts +24 -0
  32. package/dist/SystemSettings.js +177 -0
  33. package/dist/TeamDesktop.d.ts +36 -0
  34. package/dist/TeamDesktop.js +39 -0
  35. package/dist/WorkbenchAppBar.d.ts +5 -0
  36. package/dist/WorkbenchAppBar.js +12 -0
  37. package/dist/app-authority.d.ts +150 -0
  38. package/dist/app-authority.js +41 -0
  39. package/dist/app-directory-reconciliation.d.ts +14 -0
  40. package/dist/app-directory-reconciliation.js +72 -0
  41. package/dist/app-directory.d.ts +206 -0
  42. package/dist/app-directory.js +21 -0
  43. package/dist/app-icon.d.ts +9 -0
  44. package/dist/app-icon.js +27 -0
  45. package/dist/app-launch.d.ts +53 -0
  46. package/dist/app-launch.js +204 -0
  47. package/dist/app-menus.d.ts +6 -0
  48. package/dist/app-menus.js +81 -0
  49. package/dist/app-open-url.d.ts +8 -0
  50. package/dist/app-open-url.js +63 -0
  51. package/dist/app-surface.d.ts +62 -0
  52. package/dist/app-surface.js +10 -0
  53. package/dist/app-windows.d.ts +107 -0
  54. package/dist/app-windows.js +21 -0
  55. package/dist/chrome.css +153 -0
  56. package/dist/command-dialogs.d.ts +42 -0
  57. package/dist/command-dialogs.js +194 -0
  58. package/dist/desktop/Desktop.d.ts +34 -0
  59. package/dist/desktop/Desktop.js +275 -0
  60. package/dist/desktop/consent.d.ts +4 -0
  61. package/dist/desktop/consent.js +64 -0
  62. package/dist/desktop/gill-frame.d.ts +14 -0
  63. package/dist/desktop/gill-frame.js +41 -0
  64. package/dist/desktop/gill-param-picker.d.ts +15 -0
  65. package/dist/desktop/gill-param-picker.js +145 -0
  66. package/dist/desktop/launcher-gill.d.ts +20 -0
  67. package/dist/desktop/launcher-gill.js +49 -0
  68. package/dist/desktop/own-launchers.d.ts +9 -0
  69. package/dist/desktop/own-launchers.js +9 -0
  70. package/dist/desktop/row-gill.d.ts +24 -0
  71. package/dist/desktop/row-gill.js +136 -0
  72. package/dist/desktop-api.d.ts +63 -0
  73. package/dist/desktop-api.js +31 -0
  74. package/dist/desktop-client.d.ts +19 -0
  75. package/dist/desktop-client.js +127 -0
  76. package/dist/desktop-layout.d.ts +117 -0
  77. package/dist/desktop-layout.js +305 -0
  78. package/dist/desktop-visibility.d.ts +8 -0
  79. package/dist/desktop-visibility.js +5 -0
  80. package/dist/layers.d.ts +5 -0
  81. package/dist/layers.js +31 -0
  82. package/dist/main.d.ts +2 -0
  83. package/dist/main.js +17 -0
  84. package/dist/member-avatar-cache.d.ts +15 -0
  85. package/dist/member-avatar-cache.js +69 -0
  86. package/dist/member-avatar.d.ts +39 -0
  87. package/dist/member-avatar.js +88 -0
  88. package/dist/menu-bar.d.ts +5 -0
  89. package/dist/menu-bar.js +5 -0
  90. package/dist/model.d.ts +6 -0
  91. package/dist/model.js +13 -0
  92. package/dist/oyster-platform-controls.d.ts +4 -0
  93. package/dist/oyster-platform-controls.js +107 -0
  94. package/dist/platform-controls.d.ts +84 -0
  95. package/dist/platform-controls.js +266 -0
  96. package/dist/queries.d.ts +7 -0
  97. package/dist/queries.js +7 -0
  98. package/dist/session-registry.d.ts +16 -0
  99. package/dist/session-registry.js +69 -0
  100. package/dist/session-selection.d.ts +6 -0
  101. package/dist/session-selection.js +22 -0
  102. package/dist/settings-leave-guard.d.ts +5 -0
  103. package/dist/settings-leave-guard.js +9 -0
  104. package/dist/settings-navigation.css +129 -0
  105. package/dist/settings-routes.d.ts +118 -0
  106. package/dist/settings-routes.js +216 -0
  107. package/dist/settings-sections.d.ts +74 -0
  108. package/dist/settings-sections.js +17 -0
  109. package/dist/shell-commands.d.ts +15 -0
  110. package/dist/shell-commands.js +95 -0
  111. package/dist/shell-host.d.ts +52 -0
  112. package/dist/shell-host.js +224 -0
  113. package/dist/shell-module.d.ts +12 -0
  114. package/dist/shell-module.js +17 -0
  115. package/dist/shell-release.d.ts +3 -0
  116. package/dist/shell-release.js +4 -0
  117. package/dist/shell.css +858 -0
  118. package/dist/shell.d.ts +6 -0
  119. package/dist/shell.js +83 -0
  120. package/dist/spa.css +11 -0
  121. package/dist/status-bar.d.ts +9 -0
  122. package/dist/status-bar.js +51 -0
  123. package/dist/team-directory.d.ts +3 -0
  124. package/dist/team-directory.js +6 -0
  125. package/dist/use-window-motion.d.ts +3 -0
  126. package/dist/use-window-motion.js +37 -0
  127. package/dist/useAppOpenUrlHandoff.d.ts +11 -0
  128. package/dist/useAppOpenUrlHandoff.js +62 -0
  129. package/dist/useAppWindowManager.d.ts +323 -0
  130. package/dist/useAppWindowManager.js +47 -0
  131. package/dist/useDirectoryCommands.d.ts +12 -0
  132. package/dist/useDirectoryCommands.js +80 -0
  133. package/dist/workbench.d.ts +3 -0
  134. package/dist/workbench.js +2 -0
  135. 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;
@@ -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;