@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,575 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { desktopSettingsHandler } from "./desktop/consent.js";
3
+ import { AppError } from "@fungi.computer/agar";
4
+ import {} from "@fungi.computer/caps/blocks/app-bar";
5
+ import { toast, Toaster } from "@fungi.computer/caps/components/toaster";
6
+ import { WindowSnapPreview } from "@fungi.computer/caps/components/window";
7
+ import { MaosaicoRegion, useLayer, useMaosaicoLayout, } from "@fungi.computer/maosaico";
8
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
9
+ import { createPortal } from "react-dom";
10
+ import { addressKey } from "@fungi.computer/oyster/shell/app-directory";
11
+ import { reconcileAppDirectory, reconcileAppWindowFromDirectory, } from "./app-directory-reconciliation.js";
12
+ import { useAppDirectory } from "./app-directory.js";
13
+ import { AppIcon } from "./app-icon.js";
14
+ import { appForRole, composeOpenRequests, createAppLaunchDescriptor, attachWindowComputer, launchComputerId, launchForComputer, launchForRoleIntent, requireHandledNavigation, resolveFileEditorWindow, } from "./app-launch.js";
15
+ import { useAppSurfaceHost, } from "./app-surface.js";
16
+ import { hasVisibleAppWindows, } from "./app-windows.js";
17
+ import { desktopVisibility } from "./desktop-visibility.js";
18
+ import { AppPane, FocusedWindowControls } from "./AppPane.js";
19
+ import { WorkbenchAppBar } from "./WorkbenchAppBar.js";
20
+ import { DesktopMenuBar } from "./DesktopMenuBar.js";
21
+ import { ShellCommandDialogs, useShellCommands } from "./shell-commands.js";
22
+ import { OwnSprite } from "./member-avatar.js";
23
+ import { Desktop } from "./desktop/Desktop.js";
24
+ import { useAppOpenUrlHandoff } from "./useAppOpenUrlHandoff.js";
25
+ import { useAppWindowManager } from "./useAppWindowManager.js";
26
+ import { useDirectoryCommands } from "./useDirectoryCommands.js";
27
+ const emptyAppBarLabel = (loading, failed) => {
28
+ if (loading)
29
+ return "Loading Apps…";
30
+ if (failed)
31
+ return "Apps unavailable";
32
+ return "Apps will appear here when available.";
33
+ };
34
+ /** Phone Home uses the same public shell command as the keyboard. */
35
+ function phoneShowDesktop(phone, covered, host, onFailure) {
36
+ if (!phone || !covered || !host)
37
+ return undefined;
38
+ return () => {
39
+ void host
40
+ .execute("shell:desktop.show", { origin: "menu" })
41
+ .then((outcome) => {
42
+ if (outcome.status !== "handled") {
43
+ console.error("Phone Show desktop refused", outcome);
44
+ onFailure("The desktop could not be shown. Try again.");
45
+ }
46
+ })
47
+ .catch((cause) => {
48
+ console.error("Phone Show desktop failed", cause);
49
+ onFailure("The desktop could not be shown. Try again.");
50
+ });
51
+ };
52
+ }
53
+ export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialComputerId, computers, computersUnavailable, extraPanes, initialFocusId, onLayoutChange, extraLaunchers = [], settingsLaunchers = [], systemMenuItems, directoryReader, takeAppOpenRequest, platform, desktopStorage, desktopMethods, gills, }) {
54
+ const setSettingsLocation = platform.openSettings;
55
+ const [appDialOpen, setAppDialOpen] = useState(false);
56
+ // The Layout menu's "Arrange desktop" command and the desktop's own
57
+ // controls share one state: this is their common ancestor.
58
+ const [desktopEditing, setDesktopEditing] = useState(false);
59
+ const appDialContainer = useLayer("popover");
60
+ const noticeContainer = useLayer("toast");
61
+ const layout = useMaosaicoLayout();
62
+ const { activeId, focus, mode, order } = layout;
63
+ const appSurfaceHost = useAppSurfaceHost();
64
+ const selectionRefreshes = useRef(new Map());
65
+ const appWindows = useAppWindowManager(appSurfaceHost, teamId, {
66
+ onNavigation: (input) => openNavigation(input),
67
+ onAppOpen: (input) => openApp(input),
68
+ onAppRole: (role) => {
69
+ const chosen = appForRole(appsQuery.data?.apps ?? [], directoryRoles, role);
70
+ return Promise.resolve(chosen ? { displayName: chosen.displayName ?? null } : null);
71
+ },
72
+ onSelectionChanged: (launch) => refreshAppDirectory(launch),
73
+ // Raise only: the App already holds keyboard focus inside its frame.
74
+ onAppFocus: (launch) => {
75
+ focus(launch.windowId);
76
+ },
77
+ });
78
+ const appProjection = useSyncExternalStore(appWindows.subscribe, appWindows.read, appWindows.read);
79
+ useEffect(() => {
80
+ for (const window of appProjection.windows)
81
+ if (window.status === "ready")
82
+ selectionRefreshes.current.delete(window.windowId);
83
+ }, [appProjection]);
84
+ const [appStatus, setAppStatus] = useState(null);
85
+ useEffect(() => {
86
+ if (!appStatus)
87
+ return;
88
+ const disconnected = appStatus instanceof AppError && appStatus.code === "DISCONNECTED";
89
+ let message = appStatus instanceof Error
90
+ ? "This App couldn’t complete that request."
91
+ : appStatus;
92
+ if (disconnected)
93
+ message = "This App lost its connection.";
94
+ const notice = {
95
+ id: "workbench-status",
96
+ toasterId: "workbench-notices",
97
+ closeButton: true,
98
+ dismissible: true,
99
+ };
100
+ if (disconnected) {
101
+ toast(message, {
102
+ ...notice,
103
+ duration: Infinity,
104
+ description: "Reload the workspace to reconnect.",
105
+ action: {
106
+ label: "Reload",
107
+ onClick: () => {
108
+ window.location.reload();
109
+ },
110
+ },
111
+ });
112
+ }
113
+ else
114
+ toast(message, notice);
115
+ }, [appStatus]);
116
+ const reconciledDirectoryRef = useRef(null);
117
+ const focusWindow = useCallback((id) => {
118
+ focus(id);
119
+ requestAnimationFrame(() => {
120
+ const pane = [
121
+ ...(paneLayerRef.current?.querySelectorAll("[data-window-id]") ?? []),
122
+ ].find((candidate) => candidate.dataset.windowId === id);
123
+ pane
124
+ ?.querySelector("[data-caps-window-handle]")
125
+ ?.focus();
126
+ });
127
+ }, [focus, paneLayerRef]);
128
+ const afterHide = (id) => {
129
+ if (layout.zoomedId === id)
130
+ layout.zoom(null);
131
+ const next = appWindows
132
+ .read()
133
+ .windows.filter((item) => item.windowId !== id && item.status !== "minimized")
134
+ .at(-1);
135
+ if (next)
136
+ focusWindow(next.windowId);
137
+ else
138
+ requestAnimationFrame(() => {
139
+ paneLayerRef.current
140
+ ?.closest(".tourtois-workbench-layout")
141
+ ?.querySelector(".caps-app-bar-launcher-button")
142
+ ?.focus();
143
+ });
144
+ };
145
+ const activeWindow = appProjection.windows.find((item) => item.windowId === activeId && item.status !== "minimized");
146
+ const activeSurfaceId = activeWindow?.launch.surfaceId ?? null;
147
+ // The App owns its history; a refusal or failure leaves Back in place.
148
+ const goBack = (windowId) => {
149
+ void appWindows.back(windowId).catch((cause) => {
150
+ console.error("App workbench operation failed", cause);
151
+ });
152
+ };
153
+ // The shell's window keys run through Whistle so /keys, the `:` line and
154
+ // which-key list them; these are the behaviors behind those commands.
155
+ const stepTarget = (step) => order.length < 2
156
+ ? undefined
157
+ : order[(Math.max(0, order.indexOf(activeId ?? "")) + step + order.length) %
158
+ order.length];
159
+ const desktopOwner = useRef(null);
160
+ useEffect(() => {
161
+ let retired = false;
162
+ let registration;
163
+ const close = (value) => {
164
+ if (value)
165
+ void value.close().catch((cause) => {
166
+ console.error("Desktop settings registration close failed", cause);
167
+ return { kind: "failed", cause };
168
+ });
169
+ };
170
+ void platform
171
+ .registerDesktopSettings(desktopSettingsHandler(() => desktopOwner.current))
172
+ .then((value) => {
173
+ if (retired)
174
+ close(value);
175
+ else
176
+ registration = value;
177
+ })
178
+ .catch((cause) => {
179
+ console.error("Desktop settings registration failed", cause);
180
+ setAppStatus("Desktop settings are unavailable. Reopen the desktop to try again.");
181
+ return { kind: "failed", cause };
182
+ });
183
+ return () => {
184
+ retired = true;
185
+ close(registration);
186
+ };
187
+ }, [platform]);
188
+ const receiveDesktopOwner = useCallback((owner) => {
189
+ desktopOwner.current = owner;
190
+ }, []);
191
+ const shellCommands = useShellCommands(appSurfaceHost, {
192
+ openApp: () => {
193
+ setAppDialOpen(true);
194
+ },
195
+ showDesktop: async () => {
196
+ layout.zoom(null);
197
+ if (hasVisibleAppWindows(appWindows.read())) {
198
+ const hiding = appWindows.hideAll();
199
+ afterHide("");
200
+ await hiding;
201
+ }
202
+ else {
203
+ await appWindows.restoreHidden();
204
+ const restored = appWindows
205
+ .read()
206
+ .windows.find((item) => item.status !== "minimized");
207
+ if (restored)
208
+ focusWindow(restored.windowId);
209
+ }
210
+ },
211
+ focusStep: (step) => {
212
+ const next = stepTarget(step);
213
+ if (!next)
214
+ return;
215
+ layout.zoom(null);
216
+ focusWindow(next);
217
+ },
218
+ swapStep: (step) => {
219
+ const next = stepTarget(step);
220
+ if (next && activeId)
221
+ layout.swap(activeId, next);
222
+ },
223
+ toggleZoom: () => {
224
+ if (activeId)
225
+ layout.zoom(layout.zoomedId === activeId ? null : activeId);
226
+ },
227
+ back: () => {
228
+ if (activeId)
229
+ goBack(activeId);
230
+ },
231
+ });
232
+ useEffect(() => {
233
+ appSurfaceHost?.focus?.(activeSurfaceId);
234
+ }, [appSurfaceHost, activeSurfaceId, activeWindow?.status]);
235
+ const { query: appsQuery, apps: listedApps, needsUpdate, roles: directoryRoles, } = useAppDirectory(teamId, directoryReader);
236
+ const refetchApps = appsQuery.refetch;
237
+ useEffect(() => platform.subscribeChanges(() => {
238
+ void refetchApps();
239
+ }), [platform, refetchApps]);
240
+ const launchFor = (installation, currentLaunch) => {
241
+ return createAppLaunchDescriptor(installation, currentLaunch ? launchComputerId(currentLaunch.href) : initialComputerId, window.location.origin, computers, computersUnavailable);
242
+ };
243
+ const refreshAppDirectory = async (launch) => {
244
+ const attempts = selectionRefreshes.current.get(launch.windowId) ?? 0;
245
+ if (attempts >= 3)
246
+ return false;
247
+ selectionRefreshes.current.set(launch.windowId, attempts + 1);
248
+ const refreshed = await appsQuery.refetch({ throwOnError: true });
249
+ const apps = refreshed.data?.apps ?? [];
250
+ reconciledDirectoryRef.current = apps;
251
+ const failure = { action: null };
252
+ const replaced = await reconcileAppWindowFromDirectory(appWindows, launch, apps, launchFor, { kind: "selection-changed" }, (action) => {
253
+ failure.action = action;
254
+ });
255
+ if (!replaced && failure.action === null)
256
+ setAppStatus("The App selection changed. This window stayed open; close it after saving or discarding work, then reopen the App.");
257
+ else if (!replaced)
258
+ setAppStatus(failure.action === "close"
259
+ ? "App could not be closed."
260
+ : "App could not be replaced.");
261
+ return replaced;
262
+ };
263
+ const openNavigation = async (input) => {
264
+ if (!appSurfaceHost)
265
+ throw new Error("Open an editor App before opening this file.");
266
+ const editor = await resolveFileEditorWindow({
267
+ apps: listedApps ?? [],
268
+ roles: directoryRoles,
269
+ appWindows,
270
+ appSurfaceHost,
271
+ computerId: input.computerId,
272
+ computers,
273
+ origin: window.location.origin,
274
+ onPermissionNeeded: (app) => {
275
+ setSettingsLocation({
276
+ category: "apps",
277
+ appKey: addressKey(app.address),
278
+ section: "permissions",
279
+ });
280
+ },
281
+ });
282
+ const { launch } = editor;
283
+ const previousLaunch = launch;
284
+ const nextLaunch = launchForComputer(launch, input.computerId);
285
+ try {
286
+ await appWindows.restore(launch.windowId);
287
+ // File navigation carries the source Computer. Reconfigure Inkcap's
288
+ // mounted host first so its open-file command receives the exact
289
+ // target; the existing iframe and command contribution stay in place.
290
+ await appWindows.reconfigure(launch.windowId, nextLaunch);
291
+ const mounted = appWindows
292
+ .read()
293
+ .windows.find((window) => window.windowId === launch.windowId);
294
+ if (mounted?.launch.href !== nextLaunch.href)
295
+ throw new Error("The editor could not use the source Computer.");
296
+ layout.zoom(null);
297
+ focusWindow(launch.windowId);
298
+ appSurfaceHost.focus?.(launch.surfaceId);
299
+ const commandId = `${launch.surfaceId}:open-file`;
300
+ const outcome = await appSurfaceHost.execute(commandId, {
301
+ origin: "programmatic",
302
+ arguments: input,
303
+ });
304
+ requireHandledNavigation(outcome);
305
+ }
306
+ catch (error) {
307
+ if (appWindows
308
+ .read()
309
+ .windows.some((window) => window.windowId === launch.windowId &&
310
+ window.launch.href === nextLaunch.href))
311
+ await appWindows
312
+ .reconfigure(launch.windowId, previousLaunch)
313
+ .catch((cause) => {
314
+ console.error("App workbench operation failed", cause);
315
+ return undefined;
316
+ });
317
+ if (editor.opened)
318
+ await appWindows.close(launch.windowId).catch((cause) => {
319
+ console.error("App workbench operation failed", cause);
320
+ return undefined;
321
+ });
322
+ throw error;
323
+ }
324
+ };
325
+ const openAppPermissions = (surfaceId) => {
326
+ const app = directory.find((item) => item.surfaceId === surfaceId);
327
+ setSettingsLocation({
328
+ category: "apps",
329
+ appKey: app ? addressKey(app.address) : null,
330
+ section: "permissions",
331
+ });
332
+ };
333
+ /** The person chose another Computer in one window's status bar. */
334
+ const selectWindowComputer = async (windowId, computerId) => {
335
+ setAppStatus(null);
336
+ try {
337
+ await attachWindowComputer(appWindows, windowId, computerId);
338
+ }
339
+ catch (cause) {
340
+ console.error("App workbench operation failed", cause);
341
+ setAppStatus("Computer unavailable. Review this App's permissions.");
342
+ const surfaceId = appWindows
343
+ .read()
344
+ .windows.find((item) => item.windowId === windowId)?.launch.surfaceId;
345
+ if (surfaceId)
346
+ openAppPermissions(surfaceId);
347
+ }
348
+ };
349
+ const openApp = async (input) => {
350
+ const current = await appsQuery.refetch({ throwOnError: true });
351
+ const requested = launchForRoleIntent(current.data?.apps ?? [], directoryRoles, input, composeOpenRequests.request.generate(), (app) => launchFor(app));
352
+ const existing = appWindows
353
+ .read()
354
+ .windows.some((item) => item.windowId === requested.windowId);
355
+ if (existing) {
356
+ await appWindows.restore(requested.windowId);
357
+ await appWindows.reconfigure(requested.windowId, requested);
358
+ }
359
+ else
360
+ await appWindows.open(requested);
361
+ layout.zoom(null);
362
+ focusWindow(requested.windowId);
363
+ };
364
+ useAppOpenUrlHandoff({
365
+ teamId,
366
+ host: appSurfaceHost,
367
+ ready: appsQuery.isSuccess,
368
+ takeAppOpenRequest,
369
+ onOpen: openApp,
370
+ setError: setAppStatus,
371
+ });
372
+ useEffect(() => {
373
+ if (initialFocusId)
374
+ focus(initialFocusId);
375
+ }, [focus, initialFocusId]);
376
+ useEffect(() => {
377
+ if (!listedApps)
378
+ return;
379
+ if (reconciledDirectoryRef.current === listedApps) {
380
+ reconciledDirectoryRef.current = null;
381
+ return;
382
+ }
383
+ void reconcileAppDirectory(appWindows, listedApps, launchFor, (action) => {
384
+ setAppStatus(action === "close"
385
+ ? "App could not be closed."
386
+ : "App could not be replaced.");
387
+ });
388
+ }, [appWindows, listedApps, initialComputerId, computers]);
389
+ const directory = useMemo(() => listedApps ?? [], [listedApps]);
390
+ const launchApp = (installation, requestedLaunch) => {
391
+ const windowId = "app:" + installation.surfaceId;
392
+ const launch = requestedLaunch ?? launchFor(installation);
393
+ if (!launch) {
394
+ setAppStatus(installation.availability !== "available"
395
+ ? `${installation.displayName ?? "App"} is unavailable.`
396
+ : `${installation.displayName ?? "App"} has no available surface.`);
397
+ return;
398
+ }
399
+ setAppStatus(null);
400
+ performance.mark("fungi.app-open.tourtois-launch-start");
401
+ const existing = appWindows
402
+ .read()
403
+ .windows.find((item) => item.windowId === windowId);
404
+ const opening = existing && launch.openRequest !== null
405
+ ? appWindows
406
+ .restore(windowId)
407
+ .then(() => appWindows.reconfigure(windowId, launch))
408
+ : appWindows.open(launch);
409
+ void opening
410
+ .then(() => {
411
+ performance.mark("fungi.app-open.tourtois-launch-ready");
412
+ layout.zoom(null);
413
+ focusWindow(windowId);
414
+ })
415
+ .catch((cause) => {
416
+ console.error("App workbench operation failed", cause);
417
+ setAppStatus(`${installation.displayName ?? "App"} could not be opened.`);
418
+ });
419
+ };
420
+ useDirectoryCommands({
421
+ host: appSurfaceHost,
422
+ directory,
423
+ manager: appWindows,
424
+ launchFor,
425
+ onFocus: (windowId) => {
426
+ layout.zoom(null);
427
+ focusWindow(windowId);
428
+ },
429
+ onError: setAppStatus,
430
+ });
431
+ // A dashboard App opens as the Team's workspace, never as a window in it.
432
+ const windowApps = useMemo(() => directory.filter((installation) => !installation.roles.includes("dashboard")), [directory]);
433
+ const launchableItems = windowApps
434
+ .filter((installation) => launchFor(installation) !== null)
435
+ .map((installation) => {
436
+ const windowId = "app:" + installation.surfaceId;
437
+ return {
438
+ id: windowId,
439
+ label: installation.displayName ?? "App",
440
+ icon: _jsx(AppIcon, { roles: installation.roles, icon: installation.icon }),
441
+ onSelect: () => {
442
+ launchApp(installation);
443
+ },
444
+ };
445
+ });
446
+ const appLaunchers = launchableItems.filter((launcher) => !appProjection.windows.some((window) => window.windowId === launcher.id));
447
+ appLaunchers.push(...settingsLaunchers.map((launcher) => {
448
+ const item = {
449
+ id: launcher.id,
450
+ label: launcher.label,
451
+ onSelect: () => {
452
+ setSettingsLocation(launcher.location);
453
+ },
454
+ };
455
+ return launcher.icon ? { ...item, icon: launcher.icon } : item;
456
+ }), ...extraLaunchers);
457
+ const inboxSurfaceId = useMemo(() => appForRole(directory, directoryRoles, "inbox")?.surfaceId ?? null, [directory, directoryRoles]);
458
+ const visibility = desktopVisibility({
459
+ hasVisibleWindows: hasVisibleAppWindows(appProjection) ||
460
+ order.some((id) => !appProjection.windows.some((window) => window.windowId === id)),
461
+ mode,
462
+ zoomedId: layout.zoomedId,
463
+ phone,
464
+ });
465
+ const covered = visibility === "covered";
466
+ const desktopRef = useRef(null);
467
+ useLayoutEffect(() => {
468
+ if (desktopRef.current)
469
+ desktopRef.current.inert = covered;
470
+ }, [covered]);
471
+ return (_jsxs("div", { className: "tourtois-workbench-layout", children: [_jsx(DesktopMenuBar, { onShowDesktop: phoneShowDesktop(phone, covered, appSurfaceHost, setAppStatus), teamName: teamName, appBar: _jsx(WorkbenchAppBar, { phone: phone, container: appDialContainer, isOpen: appDialOpen, launcherLabels: "wrap", onOpenChange: setAppDialOpen, emptyLabel: emptyAppBarLabel(appsQuery.isLoading, appsQuery.isError), windows: appProjection.windows.map((projection) => ({
472
+ id: projection.windowId,
473
+ label: projection.launch.title,
474
+ icon: (_jsx(AppIcon, { roles: projection.launch.roles, icon: directory.find((app) => app.surfaceId === projection.launch.surfaceId)?.icon })),
475
+ active: activeId === projection.windowId,
476
+ onSelect: () => {
477
+ void appWindows
478
+ .restore(projection.windowId)
479
+ .then(() => {
480
+ layout.zoom(null);
481
+ focusWindow(projection.windowId);
482
+ })
483
+ .catch((cause) => {
484
+ console.error("App workbench operation failed", cause);
485
+ setAppStatus("App could not be restored.");
486
+ });
487
+ },
488
+ onClose: () => {
489
+ void appWindows
490
+ .close(projection.windowId)
491
+ .then(() => {
492
+ if (!appWindows
493
+ .read()
494
+ .windows.some((item) => item.windowId === projection.windowId))
495
+ afterHide(projection.windowId);
496
+ })
497
+ .catch((cause) => {
498
+ console.error("App workbench operation failed", cause);
499
+ setAppStatus("App could not be closed.");
500
+ });
501
+ },
502
+ })), launchers: appLaunchers, launcher: {
503
+ label: "Open Apps",
504
+ icon: (_jsx(OwnSprite, { "aria-hidden": "true", style: { width: "100%", height: "100%" } })),
505
+ } }), onOpenSettings: () => {
506
+ setSettingsLocation({ category: "home" });
507
+ }, onEditAvatar: () => {
508
+ setSettingsLocation({ category: "avatar" });
509
+ }, focusedId: activeWindow?.windowId ?? null, title: activeWindow?.launch.title ?? "Apps", tiled: mode === "tiled", onLayoutChange: onLayoutChange
510
+ ? (tiled) => {
511
+ layout.zoom(null);
512
+ onLayoutChange(tiled);
513
+ }
514
+ : undefined, onRestoreLayout: layout.zoomedId
515
+ ? () => {
516
+ layout.zoom(null);
517
+ }
518
+ : undefined, onArrangeDesktop: () => {
519
+ setDesktopEditing(true);
520
+ }, focusedControls: activeWindow ? (_jsx(FocusedWindowControls, { projection: activeWindow, app: directory.find((app) => app.surfaceId === activeWindow.launch.surfaceId), onOpenSettings: () => {
521
+ const app = directory.find((item) => item.surfaceId === activeWindow.launch.surfaceId);
522
+ setSettingsLocation({
523
+ category: "apps",
524
+ appKey: app ? addressKey(app.address) : null,
525
+ section: "advanced",
526
+ });
527
+ }, onOpenPermissions: () => {
528
+ openAppPermissions(activeWindow.launch.surfaceId);
529
+ } }, activeWindow.windowId)) : null, systemMenuItems: systemMenuItems }), noticeContainer
530
+ ? createPortal(_jsx(Toaster, { id: "workbench-notices", position: "bottom-right", offset: 24, mobileOffset: 24, closeButton: true }), noticeContainer)
531
+ : null, _jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx(MaosaicoRegion, { layer: "desktop", style: { position: "absolute", inset: 0 }, "data-desktop-visibility": visibility, ref: desktopRef, "aria-hidden": covered ? true : undefined, children: _jsx(Desktop, { phone: phone, editing: desktopEditing, onEditingChange: setDesktopEditing, inboxSurfaceId: inboxSurfaceId, onOwner: receiveDesktopOwner, launcherItems: launchableItems, onAllApps: () => {
532
+ setAppDialOpen(true);
533
+ }, paused: covered, apps: windowApps, needsUpdate: needsUpdate, onAppSettings: (app) => {
534
+ setSettingsLocation({
535
+ category: "apps",
536
+ appKey: addressKey(app.address),
537
+ section: "advanced",
538
+ });
539
+ }, loading: appsQuery.isLoading, unavailable: appsQuery.isError, onOpen: (input) => {
540
+ void openApp(input).catch((cause) => {
541
+ console.error("Gill destination could not be opened", cause);
542
+ setAppStatus("The widget’s App could not be opened.");
543
+ });
544
+ }, computersForGill: (surfaceId) => {
545
+ const app = windowApps.find((app) => app.surfaceId === surfaceId);
546
+ return app ? (launchFor(app)?.computers ?? []) : [];
547
+ }, storage: desktopStorage, methods: desktopMethods, transport: gills, onRetryApps: () => {
548
+ void appsQuery.refetch();
549
+ } }) }), _jsxs(MaosaicoRegion, { layer: "windows", style: { height: "100%", pointerEvents: "none" }, ref: paneLayerRef, className: "tourtois-pane-layer", "data-layout-mode": mode, children: [appProjection.windows.map((projection, index) => (_jsx(AppPane, { index: index, focusWindow: focusWindow, afterHide: afterHide, onBack: goBack, manager: appWindows, projection: projection, icon: directory.find((app) => app.surfaceId === projection.launch.surfaceId)?.icon, onError: setAppStatus, onSelectComputer: (windowId, computerId) => {
550
+ void selectWindowComputer(windowId, computerId);
551
+ } }, projection.windowId))), extraPanes?.({ activeId, order, focus }), layout.snapPreview && (_jsx(WindowSnapPreview, { "data-snap-side": layout.snapPreview.side, style: {
552
+ left: layout.snapPreview.frame.x,
553
+ top: layout.snapPreview.frame.y,
554
+ width: layout.snapPreview.frame.width,
555
+ height: layout.snapPreview.frame.height,
556
+ ...layout.stackingStyle,
557
+ } })), layout.divider ? (_jsx("div", { className: "tourtois-divider", role: "separator", "aria-label": "Main window width", "aria-orientation": "vertical", "aria-valuemin": Math.round(layout.divider.min), "aria-valuemax": Math.round(layout.divider.max), "aria-valuenow": Math.round(layout.divider.position), tabIndex: 0, style: { left: layout.divider.position, ...layout.stackingStyle }, ...layout.divider.props, onKeyDown: (event) => {
558
+ if (!layout.divider)
559
+ return;
560
+ const step = new Map([
561
+ ["ArrowLeft", -32],
562
+ ["ArrowRight", 32],
563
+ ]).get(event.key) ?? 0;
564
+ if (step) {
565
+ event.preventDefault();
566
+ event.stopPropagation();
567
+ layout.resizeMain(layout.divider.position + step);
568
+ }
569
+ } })) : null] })] }), _jsx(ShellCommandDialogs, { host: appSurfaceHost, state: shellCommands, focusedWindow: activeWindow, directoryApps: directory.map((app) => ({
570
+ commands: app.commands,
571
+ name: app.displayName ?? "App",
572
+ roles: app.roles,
573
+ surfaceId: app.surfaceId,
574
+ })), onStatus: setAppStatus, finalFocus: () => document.querySelector(".caps-app-bar-launcher-button") })] }));
575
+ }
@@ -0,0 +1,7 @@
1
+ import { type MemberAvatar } from "@fungi.computer/sprite-zdog/avatar";
2
+ /** Catalog-driven controls; the account owner handles saving and rollback. */
3
+ export declare function AvatarPicker({ value, disabled, onChange, }: {
4
+ value: MemberAvatar;
5
+ disabled: boolean;
6
+ onChange: (next: MemberAvatar) => void;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card } from "@fungi.computer/caps/components/card";
3
+ import { DataListHeading } from "@fungi.computer/caps/components/data-list";
4
+ import { RadioGroup, RadioGroupItem, RadioGroupCard, } from "@fungi.computer/caps/components/radio-group";
5
+ import { Switch } from "@fungi.computer/caps/components/switch";
6
+ import { Input } from "@fungi.computer/caps/components/input";
7
+ import { Field } from "@fungi.computer/caps/components/field";
8
+ import { renderStyles, renderStyleLabels, renderStyleSchema, resolveAppearance, } from "@fungi.computer/sprite";
9
+ import { avatarCharacters, avatarEnvelopeSchema, } from "@fungi.computer/sprite/avatar";
10
+ import { avatarCatalog } from "@fungi.computer/sprite-zdog/catalog";
11
+ import { memberAvatarSchema, deriveAgentAvatar, } from "@fungi.computer/sprite-zdog/avatar";
12
+ import { AvatarPreview } from "./AvatarPreview.js";
13
+ const names = { mushroom: "Shiitake", cat: "Cat", burger: "Burger" };
14
+ const swatches = [
15
+ ["Rose", "#f98284", "#ffe6c6"],
16
+ ["Peach", "#f6a55c", "#ffe9c9"],
17
+ ["Gold", "#f2d06b", "#fff6d1"],
18
+ ["Leaf", "#9bd47a", "#e6f8d8"],
19
+ ["Mint", "#5fc7b4", "#d9f6ef"],
20
+ ["Sky", "#6aaee8", "#dcefff"],
21
+ ["Lilac", "#9b8cf0", "#ebe6ff"],
22
+ ["Plum", "#e58bd6", "#fbe2f6"],
23
+ ["Clay", "#c98c6a", "#f6e2d4"],
24
+ ["Slate", "#8a8f9c", "#e3e5ea"],
25
+ ];
26
+ const exampleNames = ["Gardener", "Scout", "Ledger", "Pip", "Moss"];
27
+ /** Catalog-driven controls; the account owner handles saving and rollback. */
28
+ export function AvatarPicker({ value, disabled, onChange, }) {
29
+ const { avatar, agentColors } = value;
30
+ const catalog = avatarCatalog[avatar.character];
31
+ const resolved = resolveAppearance(catalog.controls, avatar.appearance);
32
+ const change = (next) => {
33
+ onChange(memberAvatarSchema.parse({ ...value, avatar: { ...avatar, ...next } }));
34
+ };
35
+ const primarySlot = catalog.identitySlots[0];
36
+ const selectedSwatch = swatches.find(([, primary, secondary]) => catalog.identitySlots.every((slot, index) => resolved[slot] === (index === 0 ? primary : secondary)))?.[0] ?? "";
37
+ return (_jsxs("div", { className: "tourtois-avatar-picker", children: [_jsxs(Card, { className: "tourtois-avatar-preview", children: [_jsx(AvatarPreview, { avatar: avatar, className: "tourtois-avatar-large" }), _jsx("strong", { children: "Your avatar" }), _jsx("p", { children: "One look for your account, across Teams." })] }), _jsxs("fieldset", { disabled: disabled, className: "tourtois-avatar-controls", children: [_jsx("legend", { className: "sr-only", children: "Customize your avatar" }), _jsxs("section", { "aria-labelledby": "avatar-character", children: [_jsx(DataListHeading, { level: 3, id: "avatar-character", children: "Character" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-character", className: "tourtois-avatar-characters", orientation: "horizontal", value: avatar.character, onValueChange: (raw) => {
38
+ const character = avatarEnvelopeSchema.shape.character.parse(raw);
39
+ change({ character, appearance: {}, expression: "normal" });
40
+ }, children: avatarCharacters.map((character) => (_jsx(RadioGroupCard, { value: character, title: names[character], description: _jsx(AvatarPreview, { avatar: {
41
+ ...avatar,
42
+ character,
43
+ appearance: character === avatar.character ? avatar.appearance : {},
44
+ expression: "normal",
45
+ }, className: "tourtois-avatar-character-preview" }) }, character))) })] }), _jsxs("section", { "aria-labelledby": "avatar-colours", children: [_jsx(DataListHeading, { level: 3, id: "avatar-colours", children: avatar.character === "mushroom" ? "Cap colour" : "Colour" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-colours", className: "tourtois-avatar-swatches", orientation: "horizontal", value: selectedSwatch, onValueChange: (name) => {
46
+ const swatch = swatches.find(([label]) => label === name);
47
+ if (!swatch)
48
+ return;
49
+ change({
50
+ appearance: {
51
+ ...avatar.appearance,
52
+ ...Object.fromEntries(catalog.identitySlots.map((slot, index) => [
53
+ slot,
54
+ swatch[index === 0 ? 1 : 2],
55
+ ])),
56
+ },
57
+ });
58
+ }, children: swatches.map(([name, color]) => (_jsx("label", { title: name, className: "tourtois-avatar-swatch", style: { backgroundColor: color }, children: _jsx(RadioGroupItem, { value: name, "aria-label": name }) }, name))) }), _jsx("div", { className: "tourtois-avatar-custom-colours", children: catalog.identitySlots.map((slot) => {
59
+ const control = catalog.controls[slot];
60
+ if (control?.type !== "color")
61
+ return null;
62
+ return (_jsx(Field, { label: slot === primarySlot ? "Custom colour" : "Accent colour", children: _jsx(Input, { type: "color", defaultValue: String(resolved[slot]), onBlur: (event) => {
63
+ if (event.currentTarget.value !== resolved[slot])
64
+ change({
65
+ appearance: {
66
+ ...avatar.appearance,
67
+ [slot]: event.currentTarget.value,
68
+ },
69
+ });
70
+ } }) }, `${avatar.character}:${slot}:${String(resolved[slot])}`));
71
+ }) })] }), _jsxs("section", { "aria-labelledby": "avatar-expression", children: [_jsx(DataListHeading, { level: 3, id: "avatar-expression", children: "Resting face" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-expression", className: "tourtois-avatar-faces", orientation: "horizontal", value: avatar.expression, onValueChange: (expression) => {
72
+ change({ expression });
73
+ }, children: catalog.expressions.map((expression) => (_jsx(RadioGroupCard, { value: expression, title: expression.charAt(0).toUpperCase() + expression.slice(1) }, expression))) })] }), _jsxs("section", { "aria-labelledby": "avatar-style", children: [_jsx(DataListHeading, { level: 3, id: "avatar-style", children: "Drawing style" }), _jsx(RadioGroup, { "aria-labelledby": "avatar-style", className: "tourtois-sprite-styles", orientation: "horizontal", value: avatar.renderStyle, onValueChange: (raw) => {
74
+ change({ renderStyle: renderStyleSchema.parse(raw) });
75
+ }, children: renderStyles.map((renderStyle) => (_jsx(RadioGroupCard, { value: renderStyle, title: renderStyleLabels[renderStyle], description: _jsx(AvatarPreview, { avatar: { ...avatar, renderStyle }, className: "tourtois-sprite-style-preview" }) }, renderStyle))) })] }), _jsxs("section", { "aria-labelledby": "avatar-agents", children: [_jsx(DataListHeading, { level: 3, id: "avatar-agents", children: "Your new Agents" }), _jsx(Field, { label: "Give each Agent its own colour", children: _jsx(Switch, { checked: agentColors === "shift", onCheckedChange: (checked) => {
76
+ onChange({ ...value, agentColors: checked ? "shift" : "keep" });
77
+ } }) }), _jsx("p", { className: "tourtois-settings-lede", children: "A preview of your preference for new Agents. Existing Agents keep their look." }), _jsx("div", { className: "tourtois-avatar-agents", children: exampleNames.map((name) => (_jsxs("div", { children: [_jsx(AvatarPreview, { avatar: deriveAgentAvatar(avatar, agentColors, name), className: "tourtois-avatar-agent-preview" }), _jsx("span", { children: name })] }, name))) })] })] })] }));
78
+ }