@fungi.computer/tourtois 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) 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 +86 -11
  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/SettingsHome.js +4 -4
  16. package/dist/SettingsNavigation.d.ts +1 -1
  17. package/dist/SettingsNavigation.js +1 -1
  18. package/dist/SystemSettings.d.ts +2 -2
  19. package/dist/SystemSettings.js +1 -1
  20. package/dist/TeamDesktop.d.ts +1 -1
  21. package/dist/app-authority.d.ts +47 -47
  22. package/dist/app-authority.js +1 -1
  23. package/dist/app-directory-reconciliation.js +7 -2
  24. package/dist/app-directory.d.ts +6 -5
  25. package/dist/app-directory.js +4 -1
  26. package/dist/app-launch.d.ts +1 -1
  27. package/dist/app-launch.js +6 -3
  28. package/dist/app-menus.d.ts +1 -1
  29. package/dist/app-surface.d.ts +2 -1
  30. package/dist/app-windows.d.ts +32 -2
  31. package/dist/app-windows.js +1 -1
  32. package/dist/command-dialogs.d.ts +1 -1
  33. package/dist/command-dialogs.js +4 -4
  34. package/dist/desktop/Desktop.d.ts +1 -1
  35. package/dist/desktop/consent.d.ts +1 -1
  36. package/dist/desktop/consent.js +1 -1
  37. package/dist/desktop/launcher-gill.js +1 -1
  38. package/dist/desktop/row-gill.d.ts +1 -1
  39. package/dist/oyster-platform-controls.d.ts +1 -1
  40. package/dist/oyster-platform-controls.js +64 -23
  41. package/dist/platform-controls.d.ts +6 -5
  42. package/dist/platform-controls.js +3 -3
  43. package/dist/queries.d.ts +4 -0
  44. package/dist/queries.js +4 -0
  45. package/dist/shell-commands.d.ts +1 -1
  46. package/dist/shell-commands.js +4 -4
  47. package/dist/shell-host.d.ts +7 -9
  48. package/dist/shell-host.js +102 -12
  49. package/dist/shell-module.d.ts +1 -1
  50. package/dist/shell.d.ts +1 -1
  51. package/dist/shell.js +5 -9
  52. package/dist/useAppWindowManager.d.ts +32 -1
  53. package/dist/useAppWindowManager.js +3 -0
  54. package/package.json +40 -19
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";
@@ -13,11 +14,40 @@ import { useWindowMotion } from "./use-window-motion.js";
13
14
  import { backHint } from "./shell-commands.js";
14
15
  import { createAppWindowManager, } from "./app-windows.js";
15
16
  import { launchComputerId } from "./app-launch.js";
17
+ /**
18
+ * The App's own path, commands and state, when it publishes them; an App
19
+ * that publishes none keeps the title and subtitle in its header.
20
+ */
21
+ function headerContent(navigation, popover, activate) {
22
+ if (navigation?.crumbs === undefined || navigation.crumbs.length === 0)
23
+ return {};
24
+ let content = {
25
+ crumbs: navigation.crumbs,
26
+ popover,
27
+ onCrumb: (crumbId, siblingId) => {
28
+ activate(siblingId === undefined
29
+ ? { kind: "crumb", crumbId }
30
+ : { kind: "crumb", crumbId, siblingId });
31
+ },
32
+ onAction: (actionId) => {
33
+ activate({ kind: "action", actionId });
34
+ },
35
+ };
36
+ if (navigation.primary !== undefined)
37
+ content = { ...content, primary: navigation.primary };
38
+ if (navigation.menu !== undefined)
39
+ content = { ...content, menu: navigation.menu };
40
+ if (navigation.status !== undefined)
41
+ content = { ...content, status: navigation.status };
42
+ return content;
43
+ }
16
44
  const windowStatusLabel = (projection) => {
17
45
  if (projection.status === "error")
18
- return `${projection.launch.title} unavailable`;
46
+ return describeAppError(projection.error).message;
19
47
  if (projection.status === "loading")
20
48
  return `${projection.launch.title} opening…`;
49
+ if (projection.status === "getting-ready")
50
+ return "Getting ready…";
21
51
  return projection.launch.title;
22
52
  };
23
53
  /** The focused window's App-level settings; its Computer lives in its status bar. */
@@ -26,8 +56,9 @@ export function FocusedWindowControls({ app, onOpenSettings, onOpenPermissions,
26
56
  return null;
27
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 })] }));
28
58
  }
29
- 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, }) {
30
60
  const containerRef = useRef(null);
61
+ const popover = useLayer("popover");
31
62
  const [computerPickerOpen, setComputerPickerOpen] = useState(false);
32
63
  useEffect(() => {
33
64
  if (projection.launch.computers === undefined)
@@ -101,7 +132,14 @@ export function AppPane({ projection, icon, manager, onError, index, focusWindow
101
132
  onBack(projection.windowId);
102
133
  }, style: binding.stackingStyle, children: [_jsx(WindowHeader, { title: projection.launch.title, ...(projection.navigation?.title === undefined
103
134
  ? {}
104
- : { subtitle: projection.navigation.title }), density: layout.mode === "tiled" ? "compact" : "regular", icon: _jsx(AppIcon, { roles: projection.launch.roles, icon: icon }), ...(projection.navigation?.canGoBack === true
135
+ : { subtitle: projection.navigation.title }), ...headerContent(projection.navigation, popover, (activation) => {
136
+ void manager
137
+ .activateNavigation(projection.windowId, activation)
138
+ .catch((cause) => {
139
+ console.error("App workbench operation failed", cause);
140
+ onError("App could not handle that action.");
141
+ });
142
+ }), density: layout.mode === "tiled" ? "compact" : "regular", icon: _jsx(AppIcon, { roles: projection.launch.roles, icon: icon }), ...(projection.navigation?.canGoBack === true
105
143
  ? {
106
144
  back: {
107
145
  label: `Back in ${projection.launch.title}`,
@@ -135,9 +173,21 @@ export function AppPane({ projection, icon, manager, onError, index, focusWindow
135
173
  console.error("App workbench operation failed", cause);
136
174
  onError("App could not be minimized.");
137
175
  });
138
- }, 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" &&
139
180
  !(projection.error instanceof AppError &&
140
- 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) => {
141
191
  onSelectComputer(projection.windowId, computerId);
142
192
  }, surface: () => containerRef.current, onAction: (action) => {
143
193
  void manager
@@ -152,13 +202,30 @@ export function AppPane({ projection, icon, manager, onError, index, focusWindow
152
202
  * One window's status bar, owned by the shell like its title bar: the
153
203
  * window's Computer first (only the person changes it, here), then the items
154
204
  * the App declared as data. The shell renders every item with Caps pieces and
155
- * 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.
156
210
  */
157
- export function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, }) {
211
+ export function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, onReload, }) {
158
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
+ }, []);
159
226
  const { computers, title } = projection.launch;
160
227
  const items = projection.statusItems;
161
- if (computers === undefined && items.length === 0)
228
+ if (computers === undefined && items.length === 0 && !onReload)
162
229
  return null;
163
230
  const anchorFor = (key) => {
164
231
  const origin = surface?.()?.getBoundingClientRect();
@@ -172,7 +239,12 @@ export function WindowStatusBar({ projection, onSelectComputer, onAction, surfac
172
239
  };
173
240
  const render = (item) => (_jsx(WindowStatusItem, { item: item, container: popover, onAction: onAction, anchorFor: anchorFor }, item.id));
174
241
  const end = items.filter((item) => item.end === true);
175
- 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);
176
248
  return (_jsx(AppStatusBar, { className: "tourtois-window-status-bar", "aria-label": `${title} status`, attachment: computers === undefined ? undefined : (_jsx(StatusBarPicker, { ...(computerPickerOpen === undefined
177
249
  ? {}
178
250
  : { open: computerPickerOpen }), ...(onComputerPickerOpenChange === undefined
@@ -180,5 +252,8 @@ export function WindowStatusBar({ projection, onSelectComputer, onAction, surfac
180
252
  : { onOpenChange: onComputerPickerOpenChange }), noun: "Computer", pluralNoun: "Computers", accessibleLabel: `Choose Computer for ${title}`, icon: _jsx(Sigil, { name: "computer", size: 16 }), options: computers.map((computer) => ({
181
253
  id: computer.computerId,
182
254
  label: computer.name,
183
- })), 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) }));
184
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 {};