@fungi.computer/tourtois 0.0.0-stage → 0.1.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 (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 +219 -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 +134 -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 +288 -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 +351 -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,145 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import {} from "@fungi.computer/gills";
4
+ import { Button } from "@fungi.computer/caps/components/button";
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@fungi.computer/caps/components/dialog";
6
+ import { Field, Form, FormFooter } from "@fungi.computer/caps/components/field";
7
+ import { Input } from "@fungi.computer/caps/components/input";
8
+ import { Select } from "@fungi.computer/caps/components/select";
9
+ import { validDesktopGillParams } from "../desktop-layout.js";
10
+ /** Chooses placement context through the Gill's own declaration and host inventory. */
11
+ export function GillParamPicker({ gill, initialParams = {}, computers, transport, editing, onSave, onClose, }) {
12
+ const [values, setValues] = useState({
13
+ ...initialParams,
14
+ });
15
+ const [saving, setSaving] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const declared = gill.params ?? [];
18
+ return (_jsx(Dialog, { open: true, onOpenChange: (open) => {
19
+ if (!open && !saving)
20
+ onClose();
21
+ }, children: _jsxs(DialogContent, { className: "tourtois-gill-param-dialog", children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { children: [editing ? "Configure" : "Place", " ", gill.title] }) }), _jsxs(Form, { busy: saving, onSubmit: (event) => {
22
+ event.preventDefault();
23
+ if (!validDesktopGillParams(values, declared))
24
+ return;
25
+ setSaving(true);
26
+ void onSave(values)
27
+ .then((accepted) => {
28
+ if (accepted)
29
+ onClose();
30
+ else
31
+ setError("Your widget could not be saved. Try again.");
32
+ })
33
+ .catch((cause) => {
34
+ console.error("Gill placement could not be confirmed", cause);
35
+ setError("Your widget placement could not be confirmed.");
36
+ })
37
+ .finally(() => {
38
+ setSaving(false);
39
+ });
40
+ }, children: [declared.map((param, index) => {
41
+ const previous = declared.slice(0, index);
42
+ const context = Object.fromEntries(previous.flatMap((param) => {
43
+ const value = values[param.key];
44
+ return value ? [[param.key, value]] : [];
45
+ }));
46
+ return (_jsx(ParameterChoice, { surfaceId: gill.surfaceId, viewId: gill.gillId, param: param, params: context, ready: previous.every((param) => !param.required || Boolean(values[param.key])), value: values[param.key] ?? "", computers: computers, transport: transport, onChange: (value) => {
47
+ setValues((before) => {
48
+ const next = Object.fromEntries(declared.slice(0, index).flatMap((param) => {
49
+ const value = before[param.key];
50
+ return value ? [[param.key, value]] : [];
51
+ }));
52
+ if (value)
53
+ next[param.key] = value;
54
+ return next;
55
+ });
56
+ } }, `${param.key}:${JSON.stringify(context)}`));
57
+ }), _jsxs(FormFooter, { error: error, children: [_jsx(Button, { type: "button", variant: "ghost", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", disabled: !validDesktopGillParams(values, declared), children: editing ? "Save widget" : "Place widget" })] })] })] }) }));
58
+ }
59
+ function ParameterChoice({ surfaceId, viewId, param, params, ready, value, computers, transport, onChange, }) {
60
+ const [reply, setReply] = useState(null);
61
+ const [error, setError] = useState(null);
62
+ const [after, setAfter] = useState();
63
+ const [loading, setLoading] = useState(false);
64
+ const [retry, setRetry] = useState(0);
65
+ const latestParams = useRef(params);
66
+ latestParams.current = params;
67
+ const contextKey = JSON.stringify(params);
68
+ useEffect(() => {
69
+ if (!ready || param.options === undefined || param.kind === "computer") {
70
+ setReply(null);
71
+ return;
72
+ }
73
+ let retired = false;
74
+ setLoading(true);
75
+ setError(null);
76
+ let request = {
77
+ surfaceId,
78
+ viewId,
79
+ options: param.key,
80
+ params: latestParams.current,
81
+ };
82
+ if (after !== undefined)
83
+ request = { ...request, after };
84
+ void transport
85
+ .requestGill(request)
86
+ .then((reply) => {
87
+ if (retired)
88
+ return;
89
+ if (!("options" in reply))
90
+ throw new Error(reply.state === "error"
91
+ ? reply.error
92
+ : "The widget returned rows instead of choices.");
93
+ setReply(reply);
94
+ })
95
+ .catch((cause) => {
96
+ if (retired)
97
+ return;
98
+ console.error("Gill choices could not be read", {
99
+ surfaceId,
100
+ viewId,
101
+ parameter: param.key,
102
+ cause,
103
+ });
104
+ setReply(null);
105
+ setError(`Choices for ${param.label} could not be loaded.`);
106
+ })
107
+ .finally(() => {
108
+ if (!retired)
109
+ setLoading(false);
110
+ });
111
+ return () => {
112
+ retired = true;
113
+ };
114
+ }, [
115
+ surfaceId,
116
+ viewId,
117
+ param.key,
118
+ param.kind,
119
+ param.options,
120
+ contextKey,
121
+ ready,
122
+ after,
123
+ retry,
124
+ transport,
125
+ ]);
126
+ const choices = param.kind === "computer"
127
+ ? computers.map((computer) => ({
128
+ value: computer.computerId,
129
+ label: computer.name,
130
+ }))
131
+ : (reply?.options ?? []);
132
+ return (_jsxs("div", { children: [_jsx(Field, { label: param.label, required: param.required, optional: !param.required, error: error, children: param.kind === "computer" || param.options !== undefined ? (_jsxs(Select, { "aria-label": param.label, value: value, disabled: !ready || loading, onChange: (event) => {
133
+ onChange(event.currentTarget.value);
134
+ }, children: [_jsx("option", { value: "", children: loading ? "Loading choices…" : `Choose a ${param.kind}` }), value && !choices.some((choice) => choice.value === value) ? (_jsx("option", { value: value, children: value })) : null, choices.map((choice) => (_jsx("option", { value: choice.value, children: choice.label }, choice.value)))] })) : (_jsx(Input, { "aria-label": param.label, value: value, maxLength: 256, disabled: !ready, onChange: (event) => {
135
+ onChange(event.currentTarget.value);
136
+ } })) }), reply?.next !== undefined ? (_jsx(Button, { type: "button", size: "sm", variant: "ghost", disabled: loading, onClick: () => {
137
+ setAfter(reply.next);
138
+ }, children: "More choices" })) : null, error ? (_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => {
139
+ setRetry((value) => value + 1);
140
+ }, children: "Retry choices" })) : null, ready &&
141
+ !loading &&
142
+ !error &&
143
+ (param.kind === "computer" || param.options !== undefined) &&
144
+ choices.length === 0 ? (_jsxs("p", { role: "status", children: ["No ", param.kind, " choices available."] })) : null] }));
145
+ }
@@ -0,0 +1,20 @@
1
+ import type { AppGridItem } from "@fungi.computer/caps/blocks/launcher-grid";
2
+ import type { AppBarItem } from "@fungi.computer/caps/blocks/app-bar";
3
+ import type { AppGillDesktopSize } from "@fungi.computer/gills";
4
+ /** Trusted shell Gill. Defaults to the same complete launchable set/order as the AppBar. */
5
+ export declare function LauncherGill({ items, size, onAllApps, loading, unavailable, onRetry, warnings, preferences, onPreference, editHandle, onContentHeight, }: {
6
+ items: readonly AppBarItem[];
7
+ size?: AppGillDesktopSize;
8
+ onAllApps: () => void;
9
+ loading?: boolean;
10
+ unavailable?: boolean;
11
+ onRetry?: () => void;
12
+ warnings?: readonly AppGridItem[];
13
+ preferences: Readonly<{
14
+ pinned: readonly string[];
15
+ hidden: readonly string[];
16
+ }>;
17
+ onPreference: (id: string, state: "Pinned" | "Shown" | "Hidden") => void;
18
+ onContentHeight?: ((height: number) => void) | undefined;
19
+ editHandle?: React.ReactNode;
20
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { AppGrid } from "@fungi.computer/caps/blocks/launcher-grid";
4
+ import { Button } from "@fungi.computer/caps/components/button";
5
+ import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
6
+ import { Select } from "@fungi.computer/caps/components/select";
7
+ import { EmptyState } from "@fungi.computer/caps/blocks/empty-state";
8
+ import { Sigil } from "@fungi.computer/sigils";
9
+ import { GillFrame } from "./gill-frame.js";
10
+ /** Trusted shell Gill. Defaults to the same complete launchable set/order as the AppBar. */
11
+ export function LauncherGill({ items, size = "M", onAllApps, loading = false, unavailable = false, onRetry, warnings = [], preferences, onPreference, editHandle, onContentHeight, }) {
12
+ const [editing, setEditing] = useState(false);
13
+ const stateFor = (id) => preferences.hidden.includes(id)
14
+ ? "Hidden"
15
+ : preferences.pinned.includes(id)
16
+ ? "Pinned"
17
+ : "Shown";
18
+ const shown = items.filter((item) => stateFor(item.id) !== "Hidden");
19
+ const ordered = [
20
+ ...shown.filter((item) => stateFor(item.id) === "Pinned"),
21
+ ...shown.filter((item) => stateFor(item.id) !== "Pinned"),
22
+ ];
23
+ const tiles = ordered.map((item) => ({
24
+ id: item.id,
25
+ label: item.label,
26
+ icon: item.icon,
27
+ onSelect: item.onSelect,
28
+ }));
29
+ const ready = !loading && !unavailable;
30
+ return (_jsxs(GillFrame, { title: "Launcher", size: size, onContentHeight: onContentHeight, editHandle: editHandle ?? (_jsx(_Fragment, { children: _jsx(Button, { size: "sm", variant: "ghost", "aria-expanded": editing, onClick: () => {
31
+ setEditing(!editing);
32
+ }, children: editing ? "Done" : "Edit launcher" }) })), children: [loading ? _jsx("p", { role: "status", children: "Loading Apps\u2026" }) : null, unavailable ? (_jsxs("p", { role: "alert", children: ["App directory unavailable.", " ", onRetry ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: onRetry, children: "Retry" })) : null] })) : null, ready ? (_jsxs(_Fragment, { children: [items.length === 0 ? (_jsx(EmptyState, { variant: "hint", title: "No Apps available", description: "Installed launchable Apps appear here." })) : null, items.length > 0 && shown.length === 0 ? (_jsx("p", { children: "No Apps shown. Edit the launcher to show them again." })) : null, _jsx(AppGrid, { "aria-label": "Team Apps", items: [
33
+ ...tiles,
34
+ ...warnings,
35
+ {
36
+ id: "all-apps",
37
+ label: "All Apps ›",
38
+ icon: _jsx(Sigil, { name: "allApps" }),
39
+ onSelect: onAllApps,
40
+ },
41
+ ], size: "lg" }), editing ? (_jsx(DataList, { "aria-label": "Launcher preferences", children: items.map((item) => (_jsx(DataRow, { children: _jsxs("div", { className: "tourtois-launcher-preference", children: [_jsx("span", { "aria-hidden": "true", className: "grid size-6 place-items-center [&_svg]:size-full", children: item.icon ?? _jsx(Sigil, { name: "installApps" }) }), _jsx("div", { className: "tourtois-launcher-preference-identity", children: _jsx("span", { children: item.label }) }), _jsxs(Select, { "aria-label": `Launcher state for ${item.label}`, value: stateFor(item.id), onChange: (event) => {
42
+ const value = event.currentTarget.value;
43
+ if (value !== "Pinned" &&
44
+ value !== "Shown" &&
45
+ value !== "Hidden")
46
+ return;
47
+ onPreference(item.id, value);
48
+ }, children: [_jsx("option", { value: "Shown", children: "Shown" }), _jsx("option", { value: "Pinned", children: "Pinned" }), _jsx("option", { value: "Hidden", children: "Hidden" })] })] }) }, item.id))) })) : null] })) : null] }));
49
+ }
@@ -0,0 +1,9 @@
1
+ /** Launchers rendered by Tourtois itself; host pages have their own published list. */
2
+ export declare const tourtoisLaunchers: readonly [{
3
+ readonly id: "settings-default-apps";
4
+ readonly label: "Default Apps";
5
+ readonly icon: "installApps";
6
+ readonly location: {
7
+ readonly category: "defaults";
8
+ };
9
+ }];
@@ -0,0 +1,9 @@
1
+ /** Launchers rendered by Tourtois itself; host pages have their own published list. */
2
+ export const tourtoisLaunchers = [
3
+ {
4
+ id: "settings-default-apps",
5
+ label: "Default Apps",
6
+ icon: "installApps",
7
+ location: { category: "defaults" },
8
+ },
9
+ ];
@@ -0,0 +1,24 @@
1
+ import type { AppBarItem } from "@fungi.computer/caps/blocks/app-bar";
2
+ import { type ReactNode } from "react";
3
+ import { type GillResponse } from "@fungi.computer/gills";
4
+ import type { TeamWorkbenchHostHandle } from "@fungi.computer/oyster/shell/workbench";
5
+ import type { AppDeclaredGill, AppGillDesktopSize } from "@fungi.computer/gills";
6
+ export type DesktopGillTransport = Pick<TeamWorkbenchHostHandle, "requestGill" | "watchGillChanges">;
7
+ /** The host admits reads; opening a row navigates inside its App. */
8
+ export declare function RowGill({ surfaceId, appHeader, declaration, params, size, paused, transport, onOpen, onConfigure, launch, editHandle, onContentHeight, }: {
9
+ surfaceId: string;
10
+ appHeader: Readonly<{
11
+ name: string;
12
+ icon: ReactNode;
13
+ }>;
14
+ declaration: AppDeclaredGill;
15
+ params?: Readonly<Record<string, string>> | undefined;
16
+ size: AppGillDesktopSize;
17
+ paused: boolean;
18
+ transport: DesktopGillTransport;
19
+ onOpen: (target: NonNullable<GillResponse["rows"][number]["open"]>) => void;
20
+ onConfigure?: (() => void) | undefined;
21
+ launch?: Pick<AppBarItem, "label" | "onSelect"> | undefined;
22
+ onContentHeight?: ((height: number) => void) | undefined;
23
+ editHandle?: ReactNode;
24
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Badge } from "@fungi.computer/caps/components/badge";
3
+ import { useEffect, useState } from "react";
4
+ import { decodeDeclaredGillResponse, } from "@fungi.computer/gills";
5
+ import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
6
+ import { ActionRow } from "@fungi.computer/caps/components/action-row";
7
+ import { Button } from "@fungi.computer/caps/components/button";
8
+ import { EmptyState } from "@fungi.computer/caps/blocks/empty-state";
9
+ import { Status } from "@fungi.computer/caps/components/status";
10
+ import { GillFrame } from "./gill-frame.js";
11
+ function detail(row) {
12
+ return row.tone === "error" && row.detail ? (_jsx(Badge, { size: "sm", variant: "quiet", tone: "error", children: row.detail })) : (row.detail);
13
+ }
14
+ function rowStatus(row) {
15
+ return row.status ? (_jsx(Status, { size: "compact", label: row.status.label, tone: row.status.tone })) : null;
16
+ }
17
+ /** The host admits reads; opening a row navigates inside its App. */
18
+ export function RowGill({ surfaceId, appHeader, declaration, params, size, paused, transport, onOpen, onConfigure, launch, editHandle, onContentHeight, }) {
19
+ const [response, setResponse] = useState(null);
20
+ const [error, setError] = useState(null);
21
+ const [pages, setPages] = useState([
22
+ undefined,
23
+ ]);
24
+ const after = pages[pages.length - 1];
25
+ const [retry, setRetry] = useState(0);
26
+ useEffect(() => {
27
+ if (paused)
28
+ return;
29
+ let closed = false;
30
+ const retired = () => closed;
31
+ let stop;
32
+ let loading;
33
+ let dirty = false;
34
+ let permanentlyUnavailable = false;
35
+ const refresh = () => {
36
+ if (permanentlyUnavailable || retired())
37
+ return Promise.resolve();
38
+ dirty = true;
39
+ if (loading)
40
+ return loading;
41
+ loading = (async () => {
42
+ while (dirty && !retired() && !permanentlyUnavailable) {
43
+ dirty = false;
44
+ const reply = await transport.requestGill({
45
+ surfaceId,
46
+ viewId: declaration.id,
47
+ params,
48
+ after,
49
+ });
50
+ if ("options" in reply)
51
+ throw new Error("Expected Gill rows, received options");
52
+ const next = decodeDeclaredGillResponse(reply, declaration);
53
+ if (retired())
54
+ return;
55
+ setResponse(next);
56
+ setError(null);
57
+ if (next.state === "error" &&
58
+ next.reason === "exec_isolation_required") {
59
+ permanentlyUnavailable = true;
60
+ const close = stop;
61
+ stop = undefined;
62
+ await close?.().catch((cause) => {
63
+ console.error("Desktop Gill observation close failed", {
64
+ surfaceId,
65
+ viewId: declaration.id,
66
+ cause,
67
+ });
68
+ });
69
+ }
70
+ }
71
+ })()
72
+ .catch((cause) => {
73
+ console.error("Desktop Gill read failed", {
74
+ surfaceId,
75
+ viewId: declaration.id,
76
+ cause,
77
+ });
78
+ if (!retired())
79
+ setError("This widget could not be refreshed.");
80
+ })
81
+ .finally(() => {
82
+ loading = undefined;
83
+ });
84
+ return loading;
85
+ };
86
+ void (async () => {
87
+ const watch = await transport.watchGillChanges({ surfaceId, viewId: declaration.id }, refresh);
88
+ if (retired()) {
89
+ await watch.close();
90
+ return;
91
+ }
92
+ stop = () => watch.close();
93
+ await refresh();
94
+ })().catch((cause) => {
95
+ console.error("Desktop Gill observation failed", {
96
+ surfaceId,
97
+ viewId: declaration.id,
98
+ cause,
99
+ });
100
+ if (!retired())
101
+ setError("This widget is unavailable.");
102
+ });
103
+ return () => {
104
+ closed = true;
105
+ void stop?.().catch((cause) => {
106
+ console.error("Desktop Gill observation close failed", {
107
+ surfaceId,
108
+ viewId: declaration.id,
109
+ cause,
110
+ });
111
+ });
112
+ };
113
+ }, [after, declaration, params, paused, retry, surfaceId, transport]);
114
+ const recovery = response?.state === "error" ? response.recovery : undefined;
115
+ const recoveryParam = recovery?.kind === "configure"
116
+ ? declaration.params?.find((param) => param.key === recovery.param)
117
+ : undefined;
118
+ const opensApp = recovery?.kind === "open" ||
119
+ (recovery === undefined &&
120
+ response?.state === "error" &&
121
+ response.reason === "exec_isolation_required");
122
+ return (_jsxs(GillFrame, { title: response?.title ?? declaration.title, appHeader: appHeader, size: size, onContentHeight: onContentHeight, editHandle: editHandle, children: [error ? (_jsxs("p", { role: "alert", children: [error, " ", _jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
123
+ setRetry((value) => value + 1);
124
+ }, children: "Retry" })] })) : null, !response && !error ? (_jsxs("p", { role: "status", children: ["Loading ", declaration.title, "\u2026"] })) : null, response?.state === "error" ? (_jsxs("div", { children: [_jsx("p", { role: "alert", children: response.reason === "exec_isolation_required" && launch
125
+ ? `${response.title} isn't available on this preview. Open ${launch.label}.`
126
+ : response.error }), recoveryParam && onConfigure ? (_jsxs(Button, { size: "sm", variant: "primary", onClick: onConfigure, children: ["Choose ", recoveryParam.label] })) : null, opensApp && launch ? (_jsxs(Button, { size: "sm", variant: "primary", onClick: () => {
127
+ launch.onSelect();
128
+ }, children: ["Open ", launch.label] })) : null] })) : null, response?.state === "empty" ? (_jsx(EmptyState, { variant: "hint", title: `No ${response.title} items yet`, description: `${response.title} items appear here when available.` })) : null, response?.state === "full" ? (_jsx(DataList, { "aria-label": `${response.title} rows`, density: "compact", children: response.rows.map((row) => (_jsx(DataRow, { children: row.open ? (_jsx(ActionRow, { density: "compact", label: row.title, description: detail(row), trailing: rowStatus(row), "aria-label": `Open: ${row.title}`, onClick: () => {
129
+ if (row.open)
130
+ onOpen(row.open);
131
+ } })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "tourtois-gill-text-row", children: [_jsx("span", { children: row.title }), row.detail ? _jsx("small", { children: detail(row) }) : null] }), rowStatus(row)] })) }, row.id))) })) : null, pages.length > 1 ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: paused, onClick: () => {
132
+ setPages((current) => current.slice(0, -1));
133
+ }, children: "Previous page" })) : null, response && response.state !== "error" && response.next !== undefined ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: paused, onClick: () => {
134
+ setPages((current) => [...current, response.next]);
135
+ }, children: "Next page" })) : null] }));
136
+ }
@@ -0,0 +1,63 @@
1
+ import * as z from "zod";
2
+ import type { appMethods } from "@fungi.computer/agar/backend";
3
+ export declare const desktopSnapshotSchema: z.ZodObject<{
4
+ layout: z.ZodObject<{
5
+ version: z.ZodLiteral<1>;
6
+ items: z.ZodArray<z.ZodObject<{
7
+ id: z.ZodString;
8
+ surfaceId: z.ZodString;
9
+ gillId: z.ZodString;
10
+ size: z.ZodEnum<{
11
+ S: "S";
12
+ M: "M";
13
+ L: "L";
14
+ }>;
15
+ col: z.ZodNumber;
16
+ row: z.ZodNumber;
17
+ params: z.ZodExactOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
18
+ }, z.core.$strict>>;
19
+ launcher: z.ZodObject<{
20
+ pinned: z.ZodArray<z.ZodString>;
21
+ hidden: z.ZodArray<z.ZodString>;
22
+ }, z.core.$strict>;
23
+ }, z.core.$strict>;
24
+ source: z.ZodEnum<{
25
+ none: "none";
26
+ member: "member";
27
+ team: "team";
28
+ }>;
29
+ memberRevision: z.ZodNullable<z.ZodNumber>;
30
+ teamRevision: z.ZodNullable<z.ZodNumber>;
31
+ canWriteTeam: z.ZodBoolean;
32
+ notice: z.ZodNullable<z.ZodString>;
33
+ }, z.core.$strict>;
34
+ export declare const desktopWidgetsSchema: z.ZodObject<{
35
+ widgets: z.ZodArray<z.ZodObject<{
36
+ surfaceId: z.ZodString;
37
+ gillId: z.ZodString;
38
+ title: z.ZodString;
39
+ sizes: z.ZodArray<z.ZodEnum<{
40
+ S: "S";
41
+ M: "M";
42
+ L: "L";
43
+ }>>;
44
+ kind: z.ZodEnum<{
45
+ app: "app";
46
+ launcher: "launcher";
47
+ }>;
48
+ params: z.ZodOptional<z.ZodArray<z.ZodObject<{
49
+ key: z.ZodString;
50
+ label: z.ZodString;
51
+ kind: z.ZodEnum<{
52
+ computer: "computer";
53
+ repository: "repository";
54
+ branch: "branch";
55
+ }>;
56
+ required: z.ZodBoolean;
57
+ options: z.ZodOptional<z.ZodString>;
58
+ }, z.core.$strict>>>;
59
+ }, z.core.$strict>>;
60
+ }, z.core.$strict>;
61
+ export type DesktopMethods = z.output<typeof appMethods.schema>;
62
+ export type DesktopSnapshot = z.output<typeof desktopSnapshotSchema>;
63
+ export type DesktopMutation = "layout.place" | "layout.move" | "layout.resize" | "layout.remove" | "layout.launcher" | "layout.set" | "layout.reset";
@@ -0,0 +1,31 @@
1
+ import * as z from "zod";
2
+ import { desktopArrangeSchema, desktopLayoutSchema } from "./desktop-layout.js";
3
+ import * as Schema from "effect/Schema";
4
+ import { appGillParamsSchema } from "@fungi.computer/gills";
5
+ export const desktopSnapshotSchema = z.strictObject({
6
+ layout: desktopLayoutSchema,
7
+ source: z.enum(["member", "team", "none"]),
8
+ memberRevision: desktopArrangeSchema.shape.basedOn,
9
+ teamRevision: desktopArrangeSchema.shape.basedOn,
10
+ canWriteTeam: z.boolean(),
11
+ notice: z.string().nullable(),
12
+ });
13
+ export const desktopWidgetsSchema = z.strictObject({
14
+ widgets: z.array(z.strictObject({
15
+ surfaceId: z.string().min(1),
16
+ gillId: z.string().min(1),
17
+ title: z.string(),
18
+ sizes: z.array(z.enum(["S", "M", "L"])),
19
+ kind: z.enum(["app", "launcher"]),
20
+ params: z
21
+ .array(z.strictObject({
22
+ key: z.string(),
23
+ label: z.string(),
24
+ kind: z.enum(["computer", "repository", "branch"]),
25
+ required: z.boolean(),
26
+ options: z.string().optional(),
27
+ }))
28
+ .refine((value) => Schema.is(appGillParamsSchema)(value))
29
+ .optional(),
30
+ })),
31
+ });
@@ -0,0 +1,19 @@
1
+ import type { AppStorage } from "@fungi.computer/agar/storage";
2
+ import type { WhistleJsonValue } from "@fungi.computer/whistle";
3
+ import { type DesktopMethods, type DesktopMutation, type DesktopSnapshot } from "./desktop-api.js";
4
+ import { type DesktopArrangeResult, type DesktopGill } from "./desktop-layout.js";
5
+ export type DesktopLayoutClient = Readonly<{
6
+ get: () => DesktopSnapshot & {
7
+ widgets: readonly DesktopGill[];
8
+ refreshFailure: Error | null;
9
+ };
10
+ subscribe: (listener: () => void) => () => void;
11
+ refresh: () => Promise<void>;
12
+ mutate: (method: DesktopMutation, input: WhistleJsonValue) => Promise<DesktopArrangeResult>;
13
+ close: () => void;
14
+ }>;
15
+ /** Watch invalidations only; the backend owns resolution, catalog and every write. */
16
+ export declare function createDesktopLayoutClient({ storage, methods, }: {
17
+ storage: Pick<AppStorage, "watch">;
18
+ methods: DesktopMethods;
19
+ }): Promise<DesktopLayoutClient>;
@@ -0,0 +1,127 @@
1
+ import { AppError } from "@fungi.computer/agar/errors";
2
+ import { desktopSnapshotSchema, desktopWidgetsSchema, } from "./desktop-api.js";
3
+ import { desktopArrangeResultSchema, } from "./desktop-layout.js";
4
+ /** Watch invalidations only; the backend owns resolution, catalog and every write. */
5
+ export async function createDesktopLayoutClient({ storage, methods, }) {
6
+ const watch = await storage.watch("desktop");
7
+ const listeners = new Set();
8
+ let snapshot;
9
+ let closed = false;
10
+ const isClosed = () => closed;
11
+ let dirty = false;
12
+ let pending;
13
+ let retirePending;
14
+ const publishRefreshFailure = (cause) => {
15
+ if (isClosed() || snapshot === undefined)
16
+ return;
17
+ const previous = snapshot.refreshFailure;
18
+ if (previous !== null &&
19
+ (previous === cause ||
20
+ (!(previous instanceof AppError) && previous.cause === cause)))
21
+ return;
22
+ snapshot = {
23
+ ...snapshot,
24
+ refreshFailure: cause instanceof AppError
25
+ ? cause
26
+ : new Error("Your desktop could not be refreshed.", { cause }),
27
+ };
28
+ for (const notify of listeners)
29
+ notify();
30
+ };
31
+ const refresh = () => {
32
+ if (isClosed())
33
+ return Promise.resolve();
34
+ dirty = true;
35
+ if (pending)
36
+ return pending;
37
+ const attempt = { retired: false };
38
+ const isRetired = () => attempt.retired;
39
+ let timer;
40
+ const interrupted = new Promise((resolve, reject) => {
41
+ retirePending = () => {
42
+ attempt.retired = true;
43
+ resolve();
44
+ };
45
+ // Bound the entire shared refresh, including coalesced invalidations.
46
+ timer = setTimeout(() => {
47
+ attempt.retired = true;
48
+ reject(new AppError("Your desktop read timed out. Try refreshing again.", {
49
+ code: "TIMEOUT",
50
+ retryable: true,
51
+ }));
52
+ }, 10_000);
53
+ });
54
+ const reading = (async () => {
55
+ while (dirty && !isClosed() && !isRetired()) {
56
+ dirty = false;
57
+ const [layout, widgets] = await Promise.all([
58
+ methods
59
+ .call("layout.get", "{}")
60
+ .then((value) => desktopSnapshotSchema.parse(value)),
61
+ methods
62
+ .call("widgets.available", "{}")
63
+ .then((value) => desktopWidgetsSchema.parse(value)),
64
+ ]);
65
+ if (isClosed() || isRetired())
66
+ return;
67
+ snapshot = {
68
+ ...layout,
69
+ widgets: widgets.widgets,
70
+ refreshFailure: null,
71
+ };
72
+ for (const notify of listeners)
73
+ notify();
74
+ }
75
+ })();
76
+ pending = Promise.race([reading, interrupted])
77
+ .finally(() => {
78
+ clearTimeout(timer);
79
+ retirePending = undefined;
80
+ pending = undefined;
81
+ })
82
+ .catch((cause) => {
83
+ publishRefreshFailure(cause);
84
+ throw cause;
85
+ });
86
+ return pending;
87
+ };
88
+ const unsubscribe = watch.subscribe(() => {
89
+ // Handle watcher failures through the same idempotent state publisher.
90
+ void refresh().catch(publishRefreshFailure);
91
+ });
92
+ const close = () => {
93
+ closed = true;
94
+ retirePending?.();
95
+ unsubscribe();
96
+ watch.close();
97
+ listeners.clear();
98
+ };
99
+ try {
100
+ await refresh();
101
+ }
102
+ catch (cause) {
103
+ close();
104
+ throw cause;
105
+ }
106
+ return {
107
+ get: () => {
108
+ if (snapshot === undefined)
109
+ throw new Error("The desktop has not loaded.");
110
+ return snapshot;
111
+ },
112
+ subscribe: (listener) => {
113
+ listeners.add(listener);
114
+ return () => {
115
+ listeners.delete(listener);
116
+ };
117
+ },
118
+ refresh,
119
+ async mutate(method, input) {
120
+ const result = desktopArrangeResultSchema.parse(await methods.call(method, JSON.stringify(input)));
121
+ if (result.kind === "accepted" || result.code === "conflict")
122
+ await refresh();
123
+ return result;
124
+ },
125
+ close,
126
+ };
127
+ }