@fungi.computer/tourtois 0.1.0 → 0.2.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 (52) hide show
  1. package/README.md +19 -0
  2. package/dist/AppAccessConsent.d.ts +6 -1
  3. package/dist/AppAccessConsent.js +4 -5
  4. package/dist/AppDesktopSettings.d.ts +2 -2
  5. package/dist/AppPane.d.ts +11 -4
  6. package/dist/AppPane.js +50 -10
  7. package/dist/AppWorkbench.d.ts +1 -1
  8. package/dist/AppWorkbench.js +10 -5
  9. package/dist/AvatarPreview.d.ts +8 -1
  10. package/dist/AvatarPreview.js +9 -1
  11. package/dist/Keys.d.ts +42 -121
  12. package/dist/Keys.js +200 -157
  13. package/dist/OysterComputers.d.ts +22 -0
  14. package/dist/OysterComputers.js +177 -0
  15. package/dist/SettingsNavigation.d.ts +1 -1
  16. package/dist/SettingsNavigation.js +1 -1
  17. package/dist/SystemSettings.d.ts +2 -2
  18. package/dist/SystemSettings.js +1 -1
  19. package/dist/TeamDesktop.d.ts +1 -1
  20. package/dist/app-authority.d.ts +37 -37
  21. package/dist/app-authority.js +1 -1
  22. package/dist/app-directory-reconciliation.js +7 -2
  23. package/dist/app-directory.d.ts +6 -5
  24. package/dist/app-directory.js +4 -1
  25. package/dist/app-launch.d.ts +1 -1
  26. package/dist/app-launch.js +6 -3
  27. package/dist/app-menus.d.ts +1 -1
  28. package/dist/app-surface.d.ts +2 -1
  29. package/dist/app-windows.d.ts +5 -2
  30. package/dist/app-windows.js +1 -1
  31. package/dist/command-dialogs.d.ts +1 -1
  32. package/dist/command-dialogs.js +4 -4
  33. package/dist/desktop/Desktop.d.ts +1 -1
  34. package/dist/desktop/consent.d.ts +1 -1
  35. package/dist/desktop/consent.js +1 -1
  36. package/dist/desktop/row-gill.d.ts +1 -1
  37. package/dist/oyster-platform-controls.d.ts +1 -1
  38. package/dist/oyster-platform-controls.js +64 -23
  39. package/dist/platform-controls.d.ts +6 -5
  40. package/dist/platform-controls.js +3 -3
  41. package/dist/queries.d.ts +4 -0
  42. package/dist/queries.js +4 -0
  43. package/dist/shell-commands.d.ts +1 -1
  44. package/dist/shell-commands.js +4 -4
  45. package/dist/shell-host.d.ts +7 -9
  46. package/dist/shell-host.js +36 -10
  47. package/dist/shell-module.d.ts +1 -1
  48. package/dist/shell.d.ts +1 -1
  49. package/dist/shell.js +5 -9
  50. package/dist/useAppWindowManager.d.ts +4 -1
  51. package/dist/useAppWindowManager.js +3 -0
  52. package/package.json +39 -18
package/README.md CHANGED
@@ -123,6 +123,25 @@ layout. [src/shell-module.ts](src/shell-module.ts) is the `mountShell` entry,
123
123
  can supply. The agent tools are in [backend/methods.ts](backend/methods.ts), and
124
124
  [package.json](package.json) declares what the shell asks of its host.
125
125
 
126
+ ### Open windows and new versions
127
+
128
+ The directory refreshes every 15 seconds while the desktop is visible, as well
129
+ as on host change notifications. A changed active Release leaves the existing
130
+ App mounted and offers **New version available — Reload** in its existing window
131
+ footer. The footer already owns host status; the header remains the App's
132
+ navigation and window controls.
133
+
134
+ Reload uses Oyster's ordinary window replacement and asks the mounted App's
135
+ close veto before disposing it. Refusal leaves the old window and the Reload
136
+ offer intact. Acceptance mounts the active Release; closing and reopening also
137
+ uses the active Release without a notice. This does not transfer unsaved memory
138
+ or preserve obsolete backend authority. Permission changes and explicit
139
+ selection-changed recovery retain their existing reconciliation behavior.
140
+
141
+ With the Tourtois workbench running, `pnpm test:window-new-version` exercises
142
+ the directory change, footer action and reopen at desktop and phone widths.
143
+ `test/window-status-bar.test.tsx` proves the Reload close veto.
144
+
126
145
  ## License
127
146
 
128
147
  MIT. See [LICENSE](LICENSE).
@@ -1,5 +1,5 @@
1
1
  import type { AgentAccessOperation, AppPermissionOutcome, ComputerAccessOperation } from "@fungi.computer/agar";
2
- import type { AppAgentPermission, AppComputerPermission } from "@fungi.computer/oyster/shell/app-directory";
2
+ import type { AppAgentPermission, AppComputerPermission } from "@fungi.computer/nacre/app-directory";
3
3
  export declare const consentableAgentOperations: (requested: readonly AgentAccessOperation[], declared: readonly string[]) => readonly AgentAccessOperation[];
4
4
  export declare const consentableComputerOperations: (requested: readonly ComputerAccessOperation[], declared: readonly string[]) => readonly ComputerAccessOperation[];
5
5
  /** The all-Team-Agents grant that adds `offered` to the current policy. */
@@ -13,6 +13,11 @@ export type AppAccessRequest = Readonly<{
13
13
  appName: string;
14
14
  agents: readonly AgentAccessOperation[];
15
15
  computers: readonly ComputerAccessOperation[];
16
+ /** A host refusal is visible and cannot be converted into an Allow. */
17
+ refusal?: Readonly<{
18
+ code: string;
19
+ message: string;
20
+ }>;
16
21
  /** Save the grant through the owner's own authority; resolves false when it failed. */
17
22
  allow: () => Promise<boolean>;
18
23
  settle: (outcome: AppPermissionOutcome) => void;
@@ -98,12 +98,12 @@ export function AppAccessConsent({ request, }) {
98
98
  ? `Let ${request.appName} work with your ${resources(request, "Agents", "Computers")}?`
99
99
  : "" }), _jsx(DialogDescription, { children: request
100
100
  ? `${request.appName} can ${describe([...request.agents, ...request.computers])} for every ${resources(request, "Agent", "Computer")} in this Oyster. You can change this later in System Settings.`
101
- : "" }), failed ? (_jsx("p", { role: "alert", children: "Access could not be saved. Try again." })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", disabled: saving, onClick: () => {
101
+ : "" }), request?.refusal !== undefined ? (_jsx("p", { role: "alert", "data-refusal-code": request.refusal.code, children: request.refusal.message })) : null, failed ? (_jsx("p", { role: "alert", children: "Access could not be saved. Try again." })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", disabled: saving, onClick: () => {
102
102
  settle("customize");
103
103
  }, children: "Customize\u2026" }), _jsx(Button, { variant: "ghost", disabled: saving, onClick: () => {
104
104
  settle("declined");
105
- }, children: "Not now" }), _jsx(Button, { disabled: saving || request === null, onClick: () => {
106
- if (request === null)
105
+ }, children: "Not now" }), _jsx(Button, { disabled: saving || request === null || request.refusal !== undefined, onClick: () => {
106
+ if (request === null || request.refusal !== undefined)
107
107
  return;
108
108
  setSaving(true);
109
109
  setFailed(false);
@@ -111,8 +111,7 @@ export function AppAccessConsent({ request, }) {
111
111
  if (saved)
112
112
  settle("granted");
113
113
  else {
114
- setSaving(false);
115
- setFailed(true);
114
+ settle("declined");
116
115
  }
117
116
  }, () => {
118
117
  setSaving(false);
@@ -1,5 +1,5 @@
1
- import type { WorkbenchDesktopSettings } from "@fungi.computer/oyster/shell/workbench";
2
- import type { AppDirectoryEntry, AppDirectoryGill } from "@fungi.computer/oyster/shell/app-directory";
1
+ import type { WorkbenchDesktopSettings } from "@fungi.computer/nacre/workbench";
2
+ import type { AppDirectoryEntry, AppDirectoryGill } from "@fungi.computer/nacre/app-directory";
3
3
  export type AppDesktopSettingsApp = Pick<AppDirectoryEntry, "surfaceId" | "availability"> & Readonly<{
4
4
  gills: readonly AppDirectoryGill[];
5
5
  }>;
package/dist/AppPane.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type AppStatusAction } from "@fungi.computer/agar";
2
- import { createAppWindowManager, type AppWindowProjection } from "./app-windows.js";
2
+ import { createAppWindowManager, type AppWindowProjection, type AppLaunchDescriptor } from "./app-windows.js";
3
3
  import { type AppDirectoryApp } from "./app-launch.js";
4
4
  /** The focused window's App-level settings; its Computer lives in its status bar. */
5
5
  export declare function FocusedWindowControls({ app, onOpenSettings, onOpenPermissions, projection, }: {
@@ -8,8 +8,9 @@ export declare function FocusedWindowControls({ app, onOpenSettings, onOpenPermi
8
8
  onOpenPermissions: () => void;
9
9
  projection: AppWindowProjection;
10
10
  }): import("react/jsx-runtime").JSX.Element | null;
11
- export declare function AppPane({ projection, icon, manager, onError, index, focusWindow, afterHide, onBack, onSelectComputer, }: {
11
+ export declare function AppPane({ projection, nextLaunch, icon, manager, onError, index, focusWindow, afterHide, onBack, onSelectComputer, }: {
12
12
  projection: AppWindowProjection;
13
+ nextLaunch?: AppLaunchDescriptor | null;
13
14
  icon: string | null | undefined;
14
15
  manager: ReturnType<typeof createAppWindowManager>;
15
16
  onError: (message: string) => void;
@@ -24,9 +25,13 @@ export declare function AppPane({ projection, icon, manager, onError, index, foc
24
25
  * One window's status bar, owned by the shell like its title bar: the
25
26
  * window's Computer first (only the person changes it, here), then the items
26
27
  * the App declared as data. The shell renders every item with Caps pieces and
27
- * hands the person's use back to the App. A window with neither has no bar.
28
+ * hands the person's use back to the App. Release availability shares this
29
+ * existing footer instead of competing with App navigation in the header.
30
+ * Reload sits beside the App's far-end readouts. Phone chrome keeps one row:
31
+ * a shorter notice takes precedence over Inkcap's low-value file count, never
32
+ * over the App's save state. Desktop presentation stays unchanged.
28
33
  */
29
- export declare function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, }: {
34
+ export declare function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, onReload, }: {
30
35
  projection: AppWindowProjection;
31
36
  onSelectComputer: (computerId: string) => void;
32
37
  onAction: (action: AppStatusAction) => void;
@@ -34,4 +39,6 @@ export declare function WindowStatusBar({ projection, onSelectComputer, onAction
34
39
  surface?: () => HTMLElement | null;
35
40
  computerPickerOpen?: boolean;
36
41
  onComputerPickerOpenChange?: (open: boolean) => void;
42
+ /** Uses the window manager's ordinary, veto-respecting replacement. */
43
+ onReload?: (() => void) | undefined;
37
44
  }): import("react/jsx-runtime").JSX.Element | null;
package/dist/AppPane.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { IconButton } from "@fungi.computer/caps/components/icon-button";
3
- import { AppStatusBar, StatusBarPicker, } from "@fungi.computer/caps/blocks/app-status-bar";
3
+ import { AppStatusBar, AppStatusItem, StatusBarChip, StatusBarPicker, } from "@fungi.computer/caps/blocks/app-status-bar";
4
4
  import { Sigil } from "@fungi.computer/sigils";
5
+ import { describeAppError } from "@fungi.computer/agar/errors";
5
6
  import { WindowActions, WindowBody, WindowFrame, WindowHeader, } from "@fungi.computer/caps/components/window";
6
7
  import { Settings, ShieldCheck } from "lucide-react";
7
8
  import { WindowStatusItem } from "./status-bar.js";
@@ -42,9 +43,11 @@ function headerContent(navigation, popover, activate) {
42
43
  }
43
44
  const windowStatusLabel = (projection) => {
44
45
  if (projection.status === "error")
45
- return `${projection.launch.title} unavailable`;
46
+ return describeAppError(projection.error).message;
46
47
  if (projection.status === "loading")
47
48
  return `${projection.launch.title} opening…`;
49
+ if (projection.status === "getting-ready")
50
+ return "Getting ready…";
48
51
  return projection.launch.title;
49
52
  };
50
53
  /** The focused window's App-level settings; its Computer lives in its status bar. */
@@ -53,7 +56,7 @@ export function FocusedWindowControls({ app, onOpenSettings, onOpenPermissions,
53
56
  return null;
54
57
  return (_jsxs("div", { className: "tourtois-focused-controls", role: "group", "aria-label": `${projection.launch.title} settings`, children: [_jsx(IconButton, { size: "responsive", "aria-label": `Permissions for ${projection.launch.title}`, title: "App permissions", icon: _jsx(ShieldCheck, {}), onClick: onOpenPermissions }), _jsx(IconButton, { size: "responsive", "aria-label": `Settings for ${projection.launch.title}`, title: "App settings", icon: _jsx(Settings, {}), onClick: onOpenSettings })] }));
55
58
  }
56
- export function AppPane({ projection, icon, manager, onError, index, focusWindow, afterHide, onBack, onSelectComputer, }) {
59
+ export function AppPane({ projection, nextLaunch, icon, manager, onError, index, focusWindow, afterHide, onBack, onSelectComputer, }) {
57
60
  const containerRef = useRef(null);
58
61
  const popover = useLayer("popover");
59
62
  const [computerPickerOpen, setComputerPickerOpen] = useState(false);
@@ -170,9 +173,21 @@ export function AppPane({ projection, icon, manager, onError, index, focusWindow
170
173
  console.error("App workbench operation failed", cause);
171
174
  onError("App could not be minimized.");
172
175
  });
173
- }, onClose: close }) }) }), projection.status === "loading" ? (_jsxs("div", { className: "tourtois-app-loading", role: "status", children: [_jsx(AppIcon, { roles: projection.launch.roles, icon: icon }), _jsxs("div", { children: [_jsxs("strong", { children: ["Opening ", projection.launch.title, "\u2026"] }), _jsx("span", { children: "Preparing this App for your workspace." })] })] })) : null, projection.status === "error" &&
176
+ }, onClose: close }) }) }), projection.status === "loading" ||
177
+ projection.status === "getting-ready" ? (_jsxs("div", { className: "tourtois-app-loading", role: "status", children: [_jsx(AppIcon, { roles: projection.launch.roles, icon: icon }), _jsxs("div", { children: [_jsx("strong", { children: projection.status === "getting-ready"
178
+ ? "Getting ready…"
179
+ : `Opening ${projection.launch.title}…` }), _jsx("span", { children: "Preparing this App for your workspace." })] })] })) : null, projection.status === "error" &&
174
180
  !(projection.error instanceof AppError &&
175
- projection.error.code === "DISCONNECTED") ? (_jsx("p", { className: "tourtois-window-status", role: "status", children: windowStatusLabel(projection) })) : null, _jsx(WindowBody, { ref: containerRef, className: "tourtois-pane-body", hidden: projection.status === "error" }), _jsx(WindowStatusBar, { projection: projection, computerPickerOpen: computerPickerOpen, onComputerPickerOpenChange: setComputerPickerOpen, onSelectComputer: (computerId) => {
181
+ projection.error.code === "DISCONNECTED") ? (_jsx("p", { className: "tourtois-window-status", role: "status", children: windowStatusLabel(projection) })) : null, _jsx(WindowBody, { ref: containerRef, className: "tourtois-pane-body", hidden: projection.status === "error" }), _jsx(WindowStatusBar, { projection: projection, onReload: nextLaunch && nextLaunch.releaseId !== projection.launch.releaseId
182
+ ? () => {
183
+ void manager
184
+ .replace(projection.windowId, nextLaunch)
185
+ .catch((cause) => {
186
+ console.error("App reload failed", cause);
187
+ onError("App could not be reloaded.");
188
+ });
189
+ }
190
+ : undefined, computerPickerOpen: computerPickerOpen, onComputerPickerOpenChange: setComputerPickerOpen, onSelectComputer: (computerId) => {
176
191
  onSelectComputer(projection.windowId, computerId);
177
192
  }, surface: () => containerRef.current, onAction: (action) => {
178
193
  void manager
@@ -187,13 +202,30 @@ export function AppPane({ projection, icon, manager, onError, index, focusWindow
187
202
  * One window's status bar, owned by the shell like its title bar: the
188
203
  * window's Computer first (only the person changes it, here), then the items
189
204
  * the App declared as data. The shell renders every item with Caps pieces and
190
- * hands the person's use back to the App. A window with neither has no bar.
205
+ * hands the person's use back to the App. Release availability shares this
206
+ * existing footer instead of competing with App navigation in the header.
207
+ * Reload sits beside the App's far-end readouts. Phone chrome keeps one row:
208
+ * a shorter notice takes precedence over Inkcap's low-value file count, never
209
+ * over the App's save state. Desktop presentation stays unchanged.
191
210
  */
192
- export function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, }) {
211
+ export function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, onReload, }) {
193
212
  const popover = useLayer("popover");
213
+ const [phone, setPhone] = useState(false);
214
+ useEffect(() => {
215
+ // Match Caps' phone status-bar breakpoint, including its touch targets.
216
+ const media = window.matchMedia("(max-width: 640px)");
217
+ const update = () => {
218
+ setPhone(media.matches);
219
+ };
220
+ update();
221
+ media.addEventListener("change", update);
222
+ return () => {
223
+ media.removeEventListener("change", update);
224
+ };
225
+ }, []);
194
226
  const { computers, title } = projection.launch;
195
227
  const items = projection.statusItems;
196
- if (computers === undefined && items.length === 0)
228
+ if (computers === undefined && items.length === 0 && !onReload)
197
229
  return null;
198
230
  const anchorFor = (key) => {
199
231
  const origin = surface?.()?.getBoundingClientRect();
@@ -207,7 +239,12 @@ export function WindowStatusBar({ projection, onSelectComputer, onAction, surfac
207
239
  };
208
240
  const render = (item) => (_jsx(WindowStatusItem, { item: item, container: popover, onAction: onAction, anchorFor: anchorFor }, item.id));
209
241
  const end = items.filter((item) => item.end === true);
210
- const start = items.filter((item) => item.end !== true);
242
+ const start = items.filter((item) => item.end !== true &&
243
+ !(phone &&
244
+ projection.launch.appId === "inkcap" &&
245
+ item.kind === "text" &&
246
+ item.id === "documents"));
247
+ const appReadouts = end.length === 0 ? undefined : end.map(render);
211
248
  return (_jsx(AppStatusBar, { className: "tourtois-window-status-bar", "aria-label": `${title} status`, attachment: computers === undefined ? undefined : (_jsx(StatusBarPicker, { ...(computerPickerOpen === undefined
212
249
  ? {}
213
250
  : { open: computerPickerOpen }), ...(onComputerPickerOpenChange === undefined
@@ -215,5 +252,8 @@ export function WindowStatusBar({ projection, onSelectComputer, onAction, surfac
215
252
  : { onOpenChange: onComputerPickerOpenChange }), noun: "Computer", pluralNoun: "Computers", accessibleLabel: `Choose Computer for ${title}`, icon: _jsx(Sigil, { name: "computer", size: 16 }), options: computers.map((computer) => ({
216
253
  id: computer.computerId,
217
254
  label: computer.name,
218
- })), value: launchComputerId(projection.launch.href), emptyMessage: "No Computers are granted to this App", disabled: projection.status === "loading", onSelect: onSelectComputer })), end: end.length === 0 ? undefined : end.map(render), children: start.length === 0 ? undefined : start.map(render) }));
255
+ })), value: launchComputerId(projection.launch.href), emptyMessage: "No Computers are granted to this App", disabled: projection.status === "loading" ||
256
+ projection.status === "getting-ready", onSelect: onSelectComputer })), end: onReload ? (_jsxs("span", { className: phone
257
+ ? "flex min-w-0 items-center gap-xs"
258
+ : "flex min-w-0 flex-wrap items-center gap-xs", children: [appReadouts, _jsxs(AppStatusItem, { children: [_jsx("span", { role: "status", children: phone ? "Update ready" : "New version available" }), _jsx(StatusBarChip, { label: "Reload", "aria-label": `Reload ${title}`, onClick: onReload })] })] })) : (appReadouts), children: start.length === 0 ? undefined : start.map(render) }));
219
259
  }
@@ -2,7 +2,7 @@ import type { DesktopMethods } from "./desktop-api.js";
2
2
  import type { AppOpenInput } from "@fungi.computer/agar/apps";
3
3
  import { type AppBarItem } from "@fungi.computer/caps/blocks/app-bar";
4
4
  import { type ReactNode, type RefObject } from "react";
5
- import type { WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
5
+ import type { WorkbenchDirectory } from "@fungi.computer/nacre/workbench-directory";
6
6
  import { type ComputerOption } from "./app-launch.js";
7
7
  import type { PlatformControls } from "./platform-controls.js";
8
8
  import type { AppStorage } from "@fungi.computer/agar/storage";
@@ -7,7 +7,7 @@ import { WindowSnapPreview } from "@fungi.computer/caps/components/window";
7
7
  import { MaosaicoRegion, useLayer, useMaosaicoLayout, } from "@fungi.computer/maosaico";
8
8
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
9
9
  import { createPortal } from "react-dom";
10
- import { addressKey } from "@fungi.computer/oyster/shell/app-directory";
10
+ import { addressKey } from "@fungi.computer/nacre/app-directory";
11
11
  import { reconcileAppDirectory, reconcileAppWindowFromDirectory, } from "./app-directory-reconciliation.js";
12
12
  import { useAppDirectory } from "./app-directory.js";
13
13
  import { AppIcon } from "./app-icon.js";
@@ -238,7 +238,9 @@ export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialCom
238
238
  void refetchApps();
239
239
  }), [platform, refetchApps]);
240
240
  const launchFor = (installation, currentLaunch) => {
241
- return createAppLaunchDescriptor(installation, currentLaunch ? launchComputerId(currentLaunch.href) : initialComputerId, window.location.origin, computers, computersUnavailable);
241
+ return createAppLaunchDescriptor(installation, currentLaunch
242
+ ? launchComputerId(currentLaunch.href)
243
+ : (appsQuery.data?.computer?.computerId ?? initialComputerId), window.location.origin, computers, computersUnavailable);
242
244
  };
243
245
  const refreshAppDirectory = async (launch) => {
244
246
  const attempts = selectionRefreshes.current.get(launch.windowId) ?? 0;
@@ -546,9 +548,12 @@ export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialCom
546
548
  return app ? (launchFor(app)?.computers ?? []) : [];
547
549
  }, storage: desktopStorage, methods: desktopMethods, transport: gills, onRetryApps: () => {
548
550
  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: {
551
+ } }) }), _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) => {
552
+ const app = directory.find((item) => item.surfaceId === projection.launch.surfaceId);
553
+ return (_jsx(AppPane, { index: index, focusWindow: focusWindow, afterHide: afterHide, onBack: goBack, manager: appWindows, projection: projection, nextLaunch: app ? launchFor(app, projection.launch) : null, icon: app?.icon, onError: setAppStatus, onSelectComputer: (windowId, computerId) => {
554
+ void selectWindowComputer(windowId, computerId);
555
+ } }, projection.windowId));
556
+ }), extraPanes?.({ activeId, order, focus }), layout.snapPreview && (_jsx(WindowSnapPreview, { "data-snap-side": layout.snapPreview.side, style: {
552
557
  left: layout.snapPreview.frame.x,
553
558
  top: layout.snapPreview.frame.y,
554
559
  width: layout.snapPreview.frame.width,
@@ -1,5 +1,12 @@
1
+ import type { SpriteService } from "@fungi.computer/sprite";
1
2
  import type { Avatar } from "@fungi.computer/sprite/avatar";
2
- import type { CSSProperties } from "react";
3
+ import { type CSSProperties } from "react";
4
+ /**
5
+ * Replaces the shared Shiitake sprite service for every preview below it;
6
+ * a host or test that must not draw live sprites supplies a fake. Absent, previews
7
+ * use Parakeet's shared service.
8
+ */
9
+ export declare const AvatarSpriteServiceContext: import("react").Context<SpriteService | undefined>;
3
10
  /** Member and picker previews use Parakeet's shared avatar mapper and service. */
4
11
  export declare function AvatarPreview({ avatar, activity, expression, className, style, }: {
5
12
  avatar: Avatar;
@@ -1,6 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ShiitakeAvatar } from "@fungi.computer/parakeet/agent-head";
3
+ import { createContext, useContext } from "react";
4
+ /**
5
+ * Replaces the shared Shiitake sprite service for every preview below it;
6
+ * a host or test that must not draw live sprites supplies a fake. Absent, previews
7
+ * use Parakeet's shared service.
8
+ */
9
+ export const AvatarSpriteServiceContext = createContext(undefined);
3
10
  /** Member and picker previews use Parakeet's shared avatar mapper and service. */
4
11
  export function AvatarPreview({ avatar, activity = "idle", expression = avatar.expression, className, style, }) {
5
- return (_jsx("span", { className: className, style: { display: "block", ...style }, "aria-hidden": "true", children: _jsx(ShiitakeAvatar, { avatar: avatar, className: "h-full w-full", state: { activity, expression } }) }));
12
+ const service = useContext(AvatarSpriteServiceContext);
13
+ return (_jsx("span", { className: className, style: { display: "block", ...style }, "aria-hidden": "true", children: _jsx(ShiitakeAvatar, { avatar: avatar, className: "h-full w-full", ...(service ? { service } : {}), state: { activity, expression } }) }));
6
14
  }
package/dist/Keys.d.ts CHANGED
@@ -1,131 +1,52 @@
1
- import * as z from "zod";
2
- /** One App's level a key may hold: read-only, or read and change. */
3
- declare const keyAppAccessLevelSchema: z.ZodEnum<{
4
- read: "read";
5
- write: "write";
1
+ import type { Grant } from "@fungi.computer/ear-tag/grants";
2
+ import type { IssueKeyInput, KeySummary, UpdateGrantsInput } from "@fungi.computer/ear-tag/keys";
3
+ import { type ComponentType } from "react";
4
+ export type KeyAppOption = Readonly<{
5
+ installationId: string;
6
+ displayName: string;
6
7
  }>;
7
- export type KeyAppAccessLevel = z.output<typeof keyAppAccessLevelSchema>;
8
- /** What a grant reaches: every installed App (including future installs), or named ones. */
9
- export declare const keyGrantTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
10
- kind: z.ZodLiteral<"all">;
11
- }, z.core.$strict>, z.ZodObject<{
12
- kind: z.ZodLiteral<"selected">;
13
- installationIds: z.ZodArray<z.ZodString>;
14
- }, z.core.$strict>], "kind">;
15
- export type KeyGrantTarget = z.output<typeof keyGrantTargetSchema>;
16
- /** One App-scoped grant a key holds. */
17
- export declare const keyGrantSchema: z.ZodObject<{
18
- level: z.ZodEnum<{
19
- read: "read";
20
- write: "write";
21
- }>;
22
- target: z.ZodDiscriminatedUnion<[z.ZodObject<{
23
- kind: z.ZodLiteral<"all">;
24
- }, z.core.$strict>, z.ZodObject<{
25
- kind: z.ZodLiteral<"selected">;
26
- installationIds: z.ZodArray<z.ZodString>;
27
- }, z.core.$strict>], "kind">;
28
- }, z.core.$strict>;
29
- export type KeyGrant = z.output<typeof keyGrantSchema>;
30
- /** One key as the host lists it; its secret never comes back after issuing. */
31
- export declare const keyRecordSchema: z.ZodObject<{
32
- keyId: z.ZodString;
33
- name: z.ZodString;
34
- grants: z.ZodArray<z.ZodObject<{
35
- level: z.ZodEnum<{
36
- read: "read";
37
- write: "write";
38
- }>;
39
- target: z.ZodDiscriminatedUnion<[z.ZodObject<{
40
- kind: z.ZodLiteral<"all">;
41
- }, z.core.$strict>, z.ZodObject<{
42
- kind: z.ZodLiteral<"selected">;
43
- installationIds: z.ZodArray<z.ZodString>;
44
- }, z.core.$strict>], "kind">;
45
- }, z.core.$strict>>;
46
- computerAccess: z.ZodBoolean;
47
- expiresAt: z.ZodNullable<z.ZodNumber>;
48
- createdAt: z.ZodNumber;
49
- lastUsedAt: z.ZodNullable<z.ZodNumber>;
50
- revokedAt: z.ZodNullable<z.ZodNumber>;
51
- }, z.core.$strict>;
52
- export type KeyRecord = z.output<typeof keyRecordSchema>;
53
- /** Every key this owner has issued. */
54
- export declare const keysViewSchema: z.ZodObject<{
55
- keys: z.ZodArray<z.ZodObject<{
56
- keyId: z.ZodString;
57
- name: z.ZodString;
58
- grants: z.ZodArray<z.ZodObject<{
59
- level: z.ZodEnum<{
60
- read: "read";
61
- write: "write";
62
- }>;
63
- target: z.ZodDiscriminatedUnion<[z.ZodObject<{
64
- kind: z.ZodLiteral<"all">;
65
- }, z.core.$strict>, z.ZodObject<{
66
- kind: z.ZodLiteral<"selected">;
67
- installationIds: z.ZodArray<z.ZodString>;
68
- }, z.core.$strict>], "kind">;
69
- }, z.core.$strict>>;
70
- computerAccess: z.ZodBoolean;
71
- expiresAt: z.ZodNullable<z.ZodNumber>;
72
- createdAt: z.ZodNumber;
73
- lastUsedAt: z.ZodNullable<z.ZodNumber>;
74
- revokedAt: z.ZodNullable<z.ZodNumber>;
75
- }, z.core.$strict>>;
76
- }, z.core.$strict>;
77
- export type KeysView = z.output<typeof keysViewSchema>;
78
- /** What the owner submits to issue one key. */
79
- export type KeyIssueDraft = Readonly<{
8
+ export type KeyTargetOption = Readonly<{
9
+ id: string;
80
10
  name: string;
81
- grants: readonly KeyGrant[];
82
- computerAccess: boolean;
83
- expiresAt: number | null;
84
11
  }>;
85
- /** The plaintext secret, shown exactly once, plus the issued record. */
86
- export declare const keyIssueResultSchema: z.ZodObject<{
87
- plaintext: z.ZodString;
88
- key: z.ZodObject<{
89
- keyId: z.ZodString;
90
- name: z.ZodString;
91
- grants: z.ZodArray<z.ZodObject<{
92
- level: z.ZodEnum<{
93
- read: "read";
94
- write: "write";
95
- }>;
96
- target: z.ZodDiscriminatedUnion<[z.ZodObject<{
97
- kind: z.ZodLiteral<"all">;
98
- }, z.core.$strict>, z.ZodObject<{
99
- kind: z.ZodLiteral<"selected">;
100
- installationIds: z.ZodArray<z.ZodString>;
101
- }, z.core.$strict>], "kind">;
102
- }, z.core.$strict>>;
103
- computerAccess: z.ZodBoolean;
104
- expiresAt: z.ZodNullable<z.ZodNumber>;
105
- createdAt: z.ZodNumber;
106
- lastUsedAt: z.ZodNullable<z.ZodNumber>;
107
- revokedAt: z.ZodNullable<z.ZodNumber>;
108
- }, z.core.$strict>;
109
- }, z.core.$strict>;
110
- export type KeyIssueResult = z.output<typeof keyIssueResultSchema>;
111
- /** The host's owner-direct custody for Keys. */
12
+ /** Presentation supplied by the host, not another capability catalog. */
13
+ export type KeyGrantOption = Readonly<{
14
+ capability: string;
15
+ label: string;
16
+ /** Omit for capabilities which have no selected-target form. */
17
+ targets?: readonly KeyTargetOption[];
18
+ }>;
19
+ export type KeyIssueDraft = Pick<IssueKeyInput<string>, "name" | "grants" | "expiresAt">;
20
+ export type KeysView = Readonly<{
21
+ keys: readonly KeySummary<string>[];
22
+ }>;
23
+ export type KeyIssueResult = Readonly<{
24
+ plaintext: string;
25
+ key: KeySummary<string>;
26
+ }>;
27
+ export type KeyChange = Readonly<{
28
+ key: KeySummary<string>;
29
+ notice: string;
30
+ /** Replay the same command, never generate another mutation identity. */
31
+ retry: boolean;
32
+ }>;
33
+ export type KeyGrantPreviewProps = Readonly<{
34
+ grants: readonly Grant[];
35
+ onReady: (ready: boolean) => void;
36
+ }>;
37
+ /** Hosts decode their current transport with their own Ear Tag vocabulary. */
112
38
  export type KeysPort = Readonly<{
39
+ scope: string;
40
+ expiry: "required" | "optional";
41
+ options(apps: readonly KeyAppOption[], signal: AbortSignal): Promise<readonly KeyGrantOption[]>;
113
42
  list(): Promise<KeysView>;
114
43
  issue(draft: KeyIssueDraft): Promise<KeyIssueResult>;
115
- revoke(keyId: string, requestId: string): Promise<KeyRecord>;
116
- }>;
117
- /** One App this Oyster has installed, as the Keys page needs it. */
118
- export type KeyAppOption = Readonly<{
119
- installationId: string;
120
- displayName: string;
44
+ updateGrants?(input: UpdateGrantsInput<string>): Promise<KeyChange>;
45
+ revoke(keyId: string, requestId: string): Promise<KeyChange>;
46
+ Preview?: ComponentType<KeyGrantPreviewProps>;
121
47
  }>;
122
- /**
123
- * The Keys settings on a standalone host: issue, list and revoke keys that
124
- * act with the owner's own access, limited to the Apps, level and expiry
125
- * chosen here. A key never gets its own identity; it narrows the owner's.
126
- */
127
- export declare function Keys({ port, apps, }: {
48
+ /** Scope changes remount the secret-once state as well as separating query caches. */
49
+ export declare function Keys(props: {
128
50
  port: KeysPort;
129
51
  apps: readonly KeyAppOption[];
130
52
  }): import("react/jsx-runtime").JSX.Element;
131
- export {};