@fungi.computer/tourtois 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -2
  3. package/dist/AgentMcpServers.d.ts +34 -0
  4. package/dist/AgentMcpServers.js +124 -0
  5. package/dist/AppAccessConsent.d.ts +23 -0
  6. package/dist/AppAccessConsent.js +122 -0
  7. package/dist/AppCallPermissions.d.ts +9 -0
  8. package/dist/AppCallPermissions.js +21 -0
  9. package/dist/AppDesktopSettings.d.ts +11 -0
  10. package/dist/AppDesktopSettings.js +102 -0
  11. package/dist/AppPane.d.ts +37 -0
  12. package/dist/AppPane.js +184 -0
  13. package/dist/AppWorkbench.d.ts +44 -0
  14. package/dist/AppWorkbench.js +575 -0
  15. package/dist/AvatarPicker.d.ts +7 -0
  16. package/dist/AvatarPicker.js +78 -0
  17. package/dist/AvatarPreview.d.ts +10 -0
  18. package/dist/AvatarPreview.js +6 -0
  19. package/dist/DesktopMenuBar.d.ts +18 -0
  20. package/dist/DesktopMenuBar.js +23 -0
  21. package/dist/FocusedAppMenus.d.ts +2 -0
  22. package/dist/FocusedAppMenus.js +10 -0
  23. package/dist/Keys.d.ts +131 -0
  24. package/dist/Keys.js +201 -0
  25. package/dist/MushroomMark.d.ts +2 -0
  26. package/dist/MushroomMark.js +5 -0
  27. package/dist/SettingsHome.d.ts +18 -0
  28. package/dist/SettingsHome.js +48 -0
  29. package/dist/SettingsNavigation.d.ts +17 -0
  30. package/dist/SettingsNavigation.js +153 -0
  31. package/dist/SystemSettings.d.ts +24 -0
  32. package/dist/SystemSettings.js +177 -0
  33. package/dist/TeamDesktop.d.ts +36 -0
  34. package/dist/TeamDesktop.js +39 -0
  35. package/dist/WorkbenchAppBar.d.ts +5 -0
  36. package/dist/WorkbenchAppBar.js +12 -0
  37. package/dist/app-authority.d.ts +150 -0
  38. package/dist/app-authority.js +41 -0
  39. package/dist/app-directory-reconciliation.d.ts +14 -0
  40. package/dist/app-directory-reconciliation.js +72 -0
  41. package/dist/app-directory.d.ts +206 -0
  42. package/dist/app-directory.js +21 -0
  43. package/dist/app-icon.d.ts +9 -0
  44. package/dist/app-icon.js +27 -0
  45. package/dist/app-launch.d.ts +53 -0
  46. package/dist/app-launch.js +204 -0
  47. package/dist/app-menus.d.ts +6 -0
  48. package/dist/app-menus.js +81 -0
  49. package/dist/app-open-url.d.ts +8 -0
  50. package/dist/app-open-url.js +63 -0
  51. package/dist/app-surface.d.ts +62 -0
  52. package/dist/app-surface.js +10 -0
  53. package/dist/app-windows.d.ts +107 -0
  54. package/dist/app-windows.js +21 -0
  55. package/dist/chrome.css +153 -0
  56. package/dist/command-dialogs.d.ts +42 -0
  57. package/dist/command-dialogs.js +194 -0
  58. package/dist/desktop/Desktop.d.ts +34 -0
  59. package/dist/desktop/Desktop.js +275 -0
  60. package/dist/desktop/consent.d.ts +4 -0
  61. package/dist/desktop/consent.js +64 -0
  62. package/dist/desktop/gill-frame.d.ts +14 -0
  63. package/dist/desktop/gill-frame.js +41 -0
  64. package/dist/desktop/gill-param-picker.d.ts +15 -0
  65. package/dist/desktop/gill-param-picker.js +145 -0
  66. package/dist/desktop/launcher-gill.d.ts +20 -0
  67. package/dist/desktop/launcher-gill.js +49 -0
  68. package/dist/desktop/own-launchers.d.ts +9 -0
  69. package/dist/desktop/own-launchers.js +9 -0
  70. package/dist/desktop/row-gill.d.ts +24 -0
  71. package/dist/desktop/row-gill.js +136 -0
  72. package/dist/desktop-api.d.ts +63 -0
  73. package/dist/desktop-api.js +31 -0
  74. package/dist/desktop-client.d.ts +19 -0
  75. package/dist/desktop-client.js +127 -0
  76. package/dist/desktop-layout.d.ts +117 -0
  77. package/dist/desktop-layout.js +305 -0
  78. package/dist/desktop-visibility.d.ts +8 -0
  79. package/dist/desktop-visibility.js +5 -0
  80. package/dist/layers.d.ts +5 -0
  81. package/dist/layers.js +31 -0
  82. package/dist/main.d.ts +2 -0
  83. package/dist/main.js +17 -0
  84. package/dist/member-avatar-cache.d.ts +15 -0
  85. package/dist/member-avatar-cache.js +69 -0
  86. package/dist/member-avatar.d.ts +39 -0
  87. package/dist/member-avatar.js +88 -0
  88. package/dist/menu-bar.d.ts +5 -0
  89. package/dist/menu-bar.js +5 -0
  90. package/dist/model.d.ts +6 -0
  91. package/dist/model.js +13 -0
  92. package/dist/oyster-platform-controls.d.ts +4 -0
  93. package/dist/oyster-platform-controls.js +107 -0
  94. package/dist/platform-controls.d.ts +84 -0
  95. package/dist/platform-controls.js +266 -0
  96. package/dist/queries.d.ts +7 -0
  97. package/dist/queries.js +7 -0
  98. package/dist/session-registry.d.ts +16 -0
  99. package/dist/session-registry.js +69 -0
  100. package/dist/session-selection.d.ts +6 -0
  101. package/dist/session-selection.js +22 -0
  102. package/dist/settings-leave-guard.d.ts +5 -0
  103. package/dist/settings-leave-guard.js +9 -0
  104. package/dist/settings-navigation.css +129 -0
  105. package/dist/settings-routes.d.ts +118 -0
  106. package/dist/settings-routes.js +216 -0
  107. package/dist/settings-sections.d.ts +74 -0
  108. package/dist/settings-sections.js +17 -0
  109. package/dist/shell-commands.d.ts +15 -0
  110. package/dist/shell-commands.js +95 -0
  111. package/dist/shell-host.d.ts +52 -0
  112. package/dist/shell-host.js +224 -0
  113. package/dist/shell-module.d.ts +12 -0
  114. package/dist/shell-module.js +17 -0
  115. package/dist/shell-release.d.ts +3 -0
  116. package/dist/shell-release.js +4 -0
  117. package/dist/shell.css +858 -0
  118. package/dist/shell.d.ts +6 -0
  119. package/dist/shell.js +83 -0
  120. package/dist/spa.css +11 -0
  121. package/dist/status-bar.d.ts +9 -0
  122. package/dist/status-bar.js +51 -0
  123. package/dist/team-directory.d.ts +3 -0
  124. package/dist/team-directory.js +6 -0
  125. package/dist/use-window-motion.d.ts +3 -0
  126. package/dist/use-window-motion.js +37 -0
  127. package/dist/useAppOpenUrlHandoff.d.ts +11 -0
  128. package/dist/useAppOpenUrlHandoff.js +62 -0
  129. package/dist/useAppWindowManager.d.ts +323 -0
  130. package/dist/useAppWindowManager.js +47 -0
  131. package/dist/useDirectoryCommands.d.ts +12 -0
  132. package/dist/useDirectoryCommands.js +80 -0
  133. package/dist/workbench.d.ts +3 -0
  134. package/dist/workbench.js +2 -0
  135. package/package.json +222 -3
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SectionHeading } from "@fungi.computer/caps/blocks/section-heading";
3
+ import { memberAvatarSchema, defaultMemberAvatar, } from "@fungi.computer/sprite-zdog/avatar";
4
+ import { AvatarPreview } from "./AvatarPreview.js";
5
+ import { AvatarPicker } from "./AvatarPicker.js";
6
+ import { skipToken, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
7
+ import { createContext, useContext, useEffect, useSyncExternalStore, } from "react";
8
+ import { createMemberAvatarCache } from "./member-avatar-cache.js";
9
+ /**
10
+ * The person's avatar preference. The host's account owns it; this browser
11
+ * keeps the last value it saw as an offline cache so the sprite draws before,
12
+ * or without, an account read. Both sources are untrusted and parsed with the
13
+ * avatar preference schema; anything that does not parse is the defaults.
14
+ */
15
+ const cache = createMemberAvatarCache(() => localStorage);
16
+ /** Decode the host-granted avatar preference. */
17
+ export const parseMemberAvatar = (value) => memberAvatarSchema.nullable().parse(value);
18
+ /** Account edits and host settings changes invalidate this query. */
19
+ export const accountAvatarQuery = ["tourtois", "account-avatar"];
20
+ const MemberAvatarReaderContext = createContext(null);
21
+ /**
22
+ * Read the avatar through a host-granted source: an isolated dashboard's
23
+ * capability, or the host's own account read. Without one, the sprite draws
24
+ * from this browser's cache.
25
+ */
26
+ export function MemberAvatarSource({ read, children, }) {
27
+ return (_jsx(MemberAvatarReaderContext.Provider, { value: read, children: children }));
28
+ }
29
+ /**
30
+ * The current avatar: the account's value once read (refreshing the
31
+ * cache), otherwise the cached value, updated across tabs.
32
+ */
33
+ export function useMemberAvatar() {
34
+ const read = useContext(MemberAvatarReaderContext);
35
+ const account = useQuery({
36
+ queryKey: accountAvatarQuery,
37
+ queryFn: read === null ? skipToken : ({ signal }) => read(signal),
38
+ retry: false,
39
+ staleTime: 60_000,
40
+ });
41
+ const fromAccount = account.data;
42
+ useEffect(() => {
43
+ if (fromAccount)
44
+ cache.write(fromAccount);
45
+ }, [fromAccount]);
46
+ const local = useSyncExternalStore(cache.subscribe, cache.snapshot, () => defaultMemberAvatar);
47
+ return fromAccount ?? local;
48
+ }
49
+ /** Save through the host; the cache takes the new value at once. */
50
+ function useSaveMemberAvatar(save) {
51
+ const client = useQueryClient();
52
+ return useMutation({
53
+ onMutate: async (next) => {
54
+ await client.cancelQueries({ queryKey: accountAvatarQuery });
55
+ const previous = client.getQueryData(accountAvatarQuery);
56
+ const local = cache.snapshot();
57
+ cache.write(next);
58
+ const optimistic = client.setQueryData(accountAvatarQuery, next);
59
+ return { previous, local, optimistic };
60
+ },
61
+ mutationFn: save,
62
+ onError: (_cause, _next, snapshot) => {
63
+ if (snapshot &&
64
+ client.getQueryData(accountAvatarQuery) === snapshot.optimistic) {
65
+ client.setQueryData(accountAvatarQuery, snapshot.previous ?? null);
66
+ cache.write(snapshot.local);
67
+ }
68
+ },
69
+ onSuccess: (saved) => {
70
+ client.setQueryData(accountAvatarQuery, saved);
71
+ cache.write(saved);
72
+ },
73
+ });
74
+ }
75
+ /** The person's sprite as the shell presents it: saved style and appearance. */
76
+ export function OwnSprite(props) {
77
+ const saved = useMemberAvatar().avatar;
78
+ const { renderStyle, ...rest } = props;
79
+ return (_jsx(AvatarPreview, { avatar: { ...saved, renderStyle: renderStyle ?? saved.renderStyle }, ...rest }));
80
+ }
81
+ /** Host-rendered picker: Apps read the preference but never write account routes. */
82
+ export function AvatarSettings({ save: write }) {
83
+ const saved = useMemberAvatar();
84
+ const save = useSaveMemberAvatar(write);
85
+ return (_jsxs("section", { className: "tourtois-avatar-settings", "aria-labelledby": "tourtois-avatar-heading", children: [_jsx(SectionHeading, { id: "tourtois-avatar-heading", children: "Avatar" }), _jsx("p", { className: "tourtois-settings-lede", children: "Choose your character, colours and drawing style. Changes save automatically." }), save.isError ? (_jsx("p", { role: "alert", children: "Your account did not save this avatar. Your previous avatar was restored. Choose it again to retry." })) : null, _jsx(AvatarPicker, { value: saved, disabled: save.isPending, onChange: (next) => {
86
+ save.mutate(next);
87
+ } })] }));
88
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+ /** The retained desktop menu strip; each host supplies its own navigation. */
3
+ export declare function ShellMenuBar({ children }: Readonly<{
4
+ children: ReactNode;
5
+ }>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** The retained desktop menu strip; each host supplies its own navigation. */
3
+ export function ShellMenuBar({ children }) {
4
+ return (_jsx("header", { className: "tourtois-desktop-menu", "aria-label": "Application menu bar", children: children }));
5
+ }
@@ -0,0 +1,6 @@
1
+ export type TourtoisAgentTarget = Readonly<{
2
+ agentId: string;
3
+ label: string;
4
+ }>;
5
+ /** Keep the routed Agent first while deduplicating the directory projection. */
6
+ export declare function orderedAgentTargets(current: TourtoisAgentTarget, decoded: readonly TourtoisAgentTarget[]): readonly TourtoisAgentTarget[];
package/dist/model.js ADDED
@@ -0,0 +1,13 @@
1
+ /** Keep the routed Agent first while deduplicating the directory projection. */
2
+ export function orderedAgentTargets(current, decoded) {
3
+ const seen = new Set([current.agentId]);
4
+ return [
5
+ current,
6
+ ...decoded.filter((agent) => {
7
+ if (seen.has(agent.agentId))
8
+ return false;
9
+ seen.add(agent.agentId);
10
+ return true;
11
+ }),
12
+ ];
13
+ }
@@ -0,0 +1,4 @@
1
+ import "@fungi.computer/parakeet/styles.css";
2
+ import "./shell.css";
3
+ import type { MountPlatformControls } from "@fungi.computer/oyster/shell/shell-module";
4
+ export declare const mountPlatformControls: MountPlatformControls;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Tourtois's Settings on a standalone Oyster, which has no Settings of its
3
+ // own: Apps, Connectors and Keys over the Oyster owner routes. The Oyster page
4
+ // mounts this from the picked shell Release's module; a host with its own
5
+ // Settings (the Hub) never calls it.
6
+ import "@fungi.computer/parakeet/styles.css";
7
+ import "./shell.css";
8
+ import { AppError } from "@fungi.computer/agar";
9
+ import { createQueryClient } from "@fungi.computer/agar/query";
10
+ import { workbenchDirectorySchema } from "@fungi.computer/oyster/shell/workbench-directory";
11
+ import { QueryClientProvider } from "@tanstack/react-query";
12
+ import * as S from "effect/Schema";
13
+ import { createRoot } from "react-dom/client";
14
+ import { agentMcpServersViewSchema, createPlatformControls, keyIssueResultSchema, keyRecordSchema, keysViewSchema, PlatformControlsView, } from "./platform-controls.js";
15
+ import { settingsLocationSchema } from "./settings-routes.js";
16
+ async function json(path) {
17
+ const response = await fetch(path, {
18
+ credentials: "same-origin",
19
+ cache: "no-store",
20
+ redirect: "error",
21
+ });
22
+ if (!response.ok)
23
+ throw new AppError("Oyster data is unavailable", {
24
+ code: response.status === 401 || response.status === 403
25
+ ? "CAPABILITY_DENIED"
26
+ : "HOST_FAILURE",
27
+ retryable: response.status >= 500,
28
+ });
29
+ return S.decodeUnknownSync(S.Json)(await response.json());
30
+ }
31
+ /** One owner-direct request; a refusal carries the host's own message. */
32
+ async function ownerRequest(path, init, refused) {
33
+ const response = await fetch(path, {
34
+ ...init,
35
+ credentials: "same-origin",
36
+ cache: "no-store",
37
+ redirect: "error",
38
+ });
39
+ if (response.ok)
40
+ return response;
41
+ const refusal = S.decodeUnknownOption(S.fromJsonString(S.Struct({ error: S.String })))(await response.text());
42
+ throw new AppError(refusal._tag === "Some" ? refusal.value.error : refused, {
43
+ code: response.status === 401 || response.status === 403
44
+ ? "CAPABILITY_DENIED"
45
+ : "HOST_FAILURE",
46
+ retryable: response.status >= 500,
47
+ });
48
+ }
49
+ /** Owner-direct Agent MCP server custody; the host never returns a token. */
50
+ const agentMcpServers = {
51
+ list: async () => agentMcpServersViewSchema.parse(await json("/api/agent-mcp-servers")),
52
+ add: async (agentId, draft) => {
53
+ await ownerRequest(`/api/agent-mcp-servers/${encodeURIComponent(agentId)}`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify(draft),
57
+ }, "The MCP server change was refused");
58
+ },
59
+ remove: async (agentId, serverId) => {
60
+ await ownerRequest(`/api/agent-mcp-servers/${encodeURIComponent(agentId)}/${encodeURIComponent(serverId)}`, { method: "DELETE" }, "The MCP server change was refused");
61
+ },
62
+ };
63
+ /** Owner-direct Keys custody; a plaintext secret comes back only once, on issue. */
64
+ const keys = {
65
+ list: async () => keysViewSchema.parse(await json("/api/keys")),
66
+ issue: async (draft) => {
67
+ const response = await ownerRequest("/api/keys", {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify(draft),
71
+ }, "The key request was refused");
72
+ return keyIssueResultSchema.parse(await response.json());
73
+ },
74
+ revoke: async (keyId, requestId) => {
75
+ const response = await ownerRequest("/api/keys/revoke", {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({ keyId, requestId }),
79
+ }, "The key request was refused");
80
+ return keyRecordSchema.parse(await response.json());
81
+ },
82
+ };
83
+ const directory = async () => workbenchDirectorySchema.parse(await json("/api/workbench/directory"));
84
+ export const mountPlatformControls = (root, input) => {
85
+ const controls = createPlatformControls({
86
+ sections: ["home", "apps", "connectors", "keys"],
87
+ });
88
+ const client = createQueryClient();
89
+ const settings = createRoot(root);
90
+ settings.render(_jsx(QueryClientProvider, { client: client, children: _jsx(PlatformControlsView, { teamId: input.teamId, controls: controls, data: { directory, agentMcpServers, keys } }) }));
91
+ return Promise.resolve({
92
+ controls: {
93
+ ...controls,
94
+ parseSettings: (value) => S.decodeUnknownSync(S.Json)(settingsLocationSchema.parse(value)),
95
+ openSettings: (value, options) => {
96
+ controls.openSettings(settingsLocationSchema.parse(value), options);
97
+ },
98
+ },
99
+ suggestDesktopSettings: (surfaceId) => {
100
+ controls.suggestDesktopSettings({ surfaceId, title: surfaceId });
101
+ },
102
+ dispose: () => {
103
+ settings.unmount();
104
+ client.clear();
105
+ },
106
+ });
107
+ };
@@ -0,0 +1,84 @@
1
+ import type { WorkbenchDirectory } from "@fungi.computer/oyster/shell/workbench-directory";
2
+ import { type WorkbenchDesktopSettings, type WorkbenchDesktopRegistration } from "@fungi.computer/oyster/shell/workbench";
3
+ import type { AppPermissionOutcome, AppPermissionRequest } from "@fungi.computer/agar";
4
+ import { type AppAccessRequest } from "./AppAccessConsent.js";
5
+ import { createSettingsRouter, type SettingsLocation } from "./settings-routes.js";
6
+ export { parseSettingsLocation } from "./settings-routes.js";
7
+ import type { SettingsSectionPorts } from "./settings-sections.js";
8
+ export type { SettingsSectionPorts, TeamAdministrationPort, } from "./settings-sections.js";
9
+ export { agentMcpServersViewSchema, type AgentMcpServerDraft, type AgentMcpServersPort, type AgentMcpServersView, } from "./AgentMcpServers.js";
10
+ export { keyGrantSchema, keyGrantTargetSchema, keyIssueResultSchema, keyRecordSchema, keysViewSchema, type KeyAppAccessLevel, type KeyAppOption, type KeyGrant, type KeyGrantTarget, type KeyIssueDraft, type KeyIssueResult, type KeyRecord, type KeysPort, type KeysView, } from "./Keys.js";
11
+ /**
12
+ * Team administration a dashboard may ask for but never performs itself:
13
+ * System Settings (App permissions, forks, default Apps, Team Git, sprite)
14
+ * and one-click App access consent. The host renders them with the Team
15
+ * owner's own authority; the dashboard only opens them and hears that the
16
+ * Team's Apps changed.
17
+ */
18
+ export type PlatformControls = Readonly<{
19
+ openSettings: (location: SettingsLocation) => void;
20
+ registerDesktopSettings: (handler: WorkbenchDesktopSettings) => Promise<WorkbenchDesktopRegistration>;
21
+ /** Observe Team App changes these controls made, to refresh a directory. */
22
+ subscribeChanges: (listener: () => void) => () => void;
23
+ }>;
24
+ /** The mounted App asking for access, identified by its directory surface. */
25
+ export type AppAccessTarget = Readonly<{
26
+ surfaceId: string;
27
+ title: string;
28
+ }>;
29
+ /** The host side: the controls plus the consent entry for child Apps. */
30
+ export type PlatformControlsHost = PlatformControls & Readonly<{
31
+ openSettings: (location: SettingsLocation, options?: Readonly<{
32
+ restoreFocus: boolean;
33
+ appInitiated?: boolean;
34
+ }>) => void;
35
+ restoreSettingsFocus: () => boolean;
36
+ desktopConsentReadyAt: () => number;
37
+ /** A child App can suggest Settings; only the member's Review opens them. */
38
+ suggestDesktopSettings: (target: AppAccessTarget) => void;
39
+ requestAppAccess: (target: AppAccessTarget, input: AppPermissionRequest) => Promise<AppPermissionOutcome>;
40
+ /** View wiring: current location, navigation and the consent handler. */
41
+ /** Settings the embedding host backs with real data and actions. */
42
+ sections?: readonly SettingsLocation["category"][] | undefined;
43
+ router: ReturnType<typeof createSettingsRouter>;
44
+ navigate: (location: SettingsLocation | null) => void;
45
+ /** The native dialog has finished closing, including its focus trap. */
46
+ settingsClosed: () => void;
47
+ subscribeSettingsClosed: (listener: () => void) => () => void;
48
+ attach: (handler: PlatformControlsHost["requestAppAccess"]) => () => void;
49
+ changed: () => void;
50
+ attachDesktopSettings: (handler: WorkbenchDesktopSettings) => () => void;
51
+ desktopSettings: WorkbenchDesktopSettings;
52
+ }>;
53
+ /** One host's platform controls; render them with `PlatformControlsView`. */
54
+ export declare function createPlatformControls(options?: Readonly<{
55
+ sections: readonly SettingsLocation["category"][];
56
+ }>): PlatformControlsHost;
57
+ /** The consent dialog a host's own access policy may ask through. */
58
+ export type AppAccessDialog = Readonly<{
59
+ /** Ask the member; resolves with their answer once the dialog settles. */
60
+ ask: (request: Omit<AppAccessRequest, "settle">) => Promise<AppPermissionOutcome>;
61
+ /** Open an App's Settings instead, as the member's "customize" answer. */
62
+ customize: (location: SettingsLocation) => "customize";
63
+ }>;
64
+ /**
65
+ * Data and implemented actions supplied by the embedding host. Every request
66
+ * runs with the signed-in member's own authority on the host's side; a
67
+ * dashboard App never receives their credentials, forms or results.
68
+ */
69
+ export type PlatformControlsData = SettingsSectionPorts & Readonly<{
70
+ directory(): Promise<WorkbenchDirectory>;
71
+ /**
72
+ * Override the owner consent flow below. Omit it to use the built-in
73
+ * one-click dialog, bounded by the target App's own declared
74
+ * capabilities; the owner's click in this trusted shell is the only grant
75
+ * a host without a separate per-App ceiling needs.
76
+ */
77
+ requestAppAccess?(target: AppAccessTarget, input: AppPermissionRequest, dialog: AppAccessDialog): Promise<AppPermissionOutcome>;
78
+ }>;
79
+ /** Tourtois owns the same Settings renderer on every embedding host. */
80
+ export declare function PlatformControlsView({ teamId, controls, data, }: {
81
+ teamId: string;
82
+ controls: PlatformControlsHost;
83
+ data: PlatformControlsData;
84
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,266 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AppError } from "@fungi.computer/agar";
3
+ import { Button } from "@fungi.computer/caps/components/button";
4
+ import { toast } from "@fungi.computer/caps/components/toaster";
5
+ import { desktopGillAdditionSchema, desktopGillSelectionSchema, } from "@fungi.computer/oyster/shell/workbench";
6
+ import { addressKey } from "@fungi.computer/oyster/shell/app-directory";
7
+ import { tourtoisQueryKeys } from "./queries.js";
8
+ import { useQuery } from "@tanstack/react-query";
9
+ import { lazy, Suspense, useEffect, useState, useSyncExternalStore, } from "react";
10
+ import { AppAccessConsent, consentableAgentOperations, consentableComputerOperations, } from "./AppAccessConsent.js";
11
+ import { RouterProvider, useRouterState } from "@tanstack/react-router";
12
+ import { createSettingsRouter, navigateSettings, settingsLocationSchema, } from "./settings-routes.js";
13
+ export { parseSettingsLocation } from "./settings-routes.js";
14
+ import { MemberAvatarSource } from "./member-avatar.js";
15
+ export { agentMcpServersViewSchema, } from "./AgentMcpServers.js";
16
+ export { keyGrantSchema, keyGrantTargetSchema, keyIssueResultSchema, keyRecordSchema, keysViewSchema, } from "./Keys.js";
17
+ /** One host's platform controls; render them with `PlatformControlsView`. */
18
+ export function createPlatformControls(options) {
19
+ const sections = options?.sections.slice();
20
+ const requireOffered = (location) => {
21
+ if (location !== null &&
22
+ sections !== undefined &&
23
+ !sections.includes(location.category))
24
+ throw new AppError("This host does not offer those Settings", {
25
+ code: "CAPABILITY_DENIED",
26
+ retryable: false,
27
+ });
28
+ };
29
+ const router = createSettingsRouter();
30
+ let restoreSettingsFocus = true;
31
+ let settingsOpen = false;
32
+ let appInitiated = false;
33
+ let appReopenAllowedAt = 0;
34
+ let consentReadyAt = 0;
35
+ let handler;
36
+ let desktopHandler;
37
+ const attachDesktopSettings = (handler) => {
38
+ desktopHandler = handler;
39
+ return () => {
40
+ if (desktopHandler === handler)
41
+ desktopHandler = undefined;
42
+ };
43
+ };
44
+ const changeListeners = new Set();
45
+ const settingsCloseListeners = new Set();
46
+ const navigate = (next) => {
47
+ requireOffered(next);
48
+ void navigateSettings(router, next).catch((cause) => {
49
+ console.error("Settings navigation failed", cause);
50
+ });
51
+ };
52
+ const dismissedSuggestions = new Set();
53
+ let retireSuggestion;
54
+ const controls = Object.freeze({
55
+ sections,
56
+ attachDesktopSettings,
57
+ registerDesktopSettings: (handler) => {
58
+ const detach = attachDesktopSettings(handler);
59
+ return Promise.resolve({
60
+ close: () => {
61
+ detach();
62
+ return Promise.resolve();
63
+ },
64
+ });
65
+ },
66
+ desktopSettings: {
67
+ readGill: async (input) => {
68
+ const request = desktopGillSelectionSchema.parse(input);
69
+ if (!desktopHandler)
70
+ throw new AppError("The desktop is unavailable. Reopen it and try again.", { code: "HOST_FAILURE", retryable: true });
71
+ return desktopHandler.readGill(request);
72
+ },
73
+ addGill: async (input) => {
74
+ const request = desktopGillAdditionSchema.parse(input);
75
+ if (!desktopHandler)
76
+ throw new AppError("The desktop is unavailable. Reopen it and try again.", { code: "HOST_FAILURE", retryable: true });
77
+ await desktopHandler.addGill(request);
78
+ },
79
+ removeGill: async (input) => {
80
+ const request = desktopGillSelectionSchema.parse(input);
81
+ if (!desktopHandler)
82
+ throw new AppError("The desktop is unavailable. Reopen it and try again.", { code: "HOST_FAILURE", retryable: true });
83
+ await desktopHandler.removeGill(request);
84
+ },
85
+ },
86
+ openSettings: (next, options) => {
87
+ requireOffered(next);
88
+ if (options?.appInitiated &&
89
+ (settingsOpen || Date.now() < appReopenAllowedAt))
90
+ throw new AppError("Close Settings and wait before requesting desktop settings again.", { code: "CAPABILITY_DENIED", retryable: false });
91
+ if (!settingsOpen) {
92
+ appInitiated = options?.appInitiated ?? false;
93
+ consentReadyAt = appInitiated ? Date.now() + 500 : 0;
94
+ }
95
+ settingsOpen = true;
96
+ // Navigation within the same dialog preserves its return-focus owner.
97
+ restoreSettingsFocus =
98
+ options?.restoreFocus ??
99
+ (router.history.location.pathname === "/closed"
100
+ ? true
101
+ : restoreSettingsFocus);
102
+ navigate(next);
103
+ },
104
+ restoreSettingsFocus: () => restoreSettingsFocus,
105
+ desktopConsentReadyAt: () => consentReadyAt,
106
+ suggestDesktopSettings: (target) => {
107
+ if (dismissedSuggestions.has(target.surfaceId))
108
+ return;
109
+ if (settingsOpen || Date.now() < appReopenAllowedAt)
110
+ throw new AppError("Close Settings and wait before suggesting desktop widgets again.", {
111
+ code: "CAPABILITY_DENIED",
112
+ retryable: false,
113
+ });
114
+ retireSuggestion?.();
115
+ let retired = false;
116
+ const id = toast(`${target.title} suggests desktop widgets`, {
117
+ toasterId: "app-connections",
118
+ position: "bottom-right",
119
+ duration: 10_000,
120
+ closeButton: true,
121
+ onDismiss: () => {
122
+ if (!retired)
123
+ dismissedSuggestions.add(target.surfaceId);
124
+ },
125
+ action: (_jsx(Button, { variant: "secondary", size: "xs", "aria-label": "Review desktop widgets", onClick: () => {
126
+ // A delayed Review must not replace another dialog or bypass its cooldown.
127
+ if (!settingsOpen && Date.now() >= appReopenAllowedAt)
128
+ controls.openSettings({
129
+ category: "apps",
130
+ appKey: target.surfaceId,
131
+ section: "desktop",
132
+ }, { restoreFocus: false, appInitiated: true });
133
+ retire();
134
+ }, children: "Review" })),
135
+ });
136
+ const retire = () => {
137
+ // Sonner calls onDismiss for programmatic retirement too.
138
+ retired = true;
139
+ toast.dismiss(id);
140
+ };
141
+ retireSuggestion = retire;
142
+ },
143
+ subscribeChanges: (listener) => {
144
+ changeListeners.add(listener);
145
+ return () => {
146
+ changeListeners.delete(listener);
147
+ };
148
+ },
149
+ requestAppAccess: (target, input) => handler === undefined
150
+ ? Promise.resolve("declined")
151
+ : handler(target, input),
152
+ router,
153
+ navigate,
154
+ settingsClosed: () => {
155
+ settingsOpen = false;
156
+ if (appInitiated)
157
+ appReopenAllowedAt = Date.now() + 10_000;
158
+ appInitiated = false;
159
+ consentReadyAt = 0;
160
+ for (const listener of settingsCloseListeners)
161
+ listener();
162
+ },
163
+ subscribeSettingsClosed: (listener) => {
164
+ settingsCloseListeners.add(listener);
165
+ return () => {
166
+ settingsCloseListeners.delete(listener);
167
+ };
168
+ },
169
+ attach: (next) => {
170
+ handler = next;
171
+ return () => {
172
+ if (handler === next)
173
+ handler = undefined;
174
+ };
175
+ },
176
+ changed: () => {
177
+ for (const listener of changeListeners)
178
+ listener();
179
+ },
180
+ });
181
+ return controls;
182
+ }
183
+ // Settings carry TanStack Form and the permission editors; load them on first
184
+ // open so the Team route does not ship them.
185
+ const SystemSettings = lazy(async () => {
186
+ const module = await import("./SystemSettings.js");
187
+ return { default: module.SystemSettings };
188
+ });
189
+ /** Loads settings on first open, then stays mounted so closing runs normally. */
190
+ function DeferredSystemSettings(props) {
191
+ const [loaded, setLoaded] = useState(false);
192
+ if (props.location !== null && !loaded)
193
+ setLoaded(true);
194
+ return loaded ? (_jsx(Suspense, { fallback: null, children: _jsx(SystemSettings, { ...props }) })) : null;
195
+ }
196
+ /** Tourtois owns the same Settings renderer on every embedding host. */
197
+ export function PlatformControlsView({ teamId, controls, data, }) {
198
+ const location = useRouterState({
199
+ router: controls.router,
200
+ select: (state) => settingsLocationSchema
201
+ .nullable()
202
+ .parse(state.matches.at(-1)?.loaderData ?? null),
203
+ });
204
+ const restoreFocus = useSyncExternalStore((changed) => controls.router.subscribe("onResolved", changed), controls.restoreSettingsFocus, () => true);
205
+ const directory = useQuery({
206
+ queryKey: tourtoisQueryKeys.hostSettings(teamId),
207
+ queryFn: () => data.directory(),
208
+ enabled: location !== null,
209
+ });
210
+ const [consent, setConsent] = useState(null);
211
+ useEffect(() => {
212
+ const dialog = {
213
+ ask: (request) => new Promise((resolve) => {
214
+ setConsent({
215
+ ...request,
216
+ settle: (settled) => {
217
+ setConsent(null);
218
+ resolve(settled);
219
+ },
220
+ });
221
+ }),
222
+ customize: (location) => {
223
+ controls.openSettings(location);
224
+ return "customize";
225
+ },
226
+ };
227
+ const requestAppAccess = data.requestAppAccess;
228
+ return controls.attach(requestAppAccess === undefined
229
+ ? async (target, input) => {
230
+ const current = await data.directory();
231
+ const app = current.apps.find((candidate) => candidate.surfaceId === target.surfaceId);
232
+ const openSettings = () => dialog.customize({
233
+ category: "apps",
234
+ appKey: app ? addressKey(app.address) : null,
235
+ section: "desktop",
236
+ });
237
+ if (app === undefined)
238
+ return openSettings();
239
+ const agents = consentableAgentOperations(input.agents ?? [], app.declaredCapabilities);
240
+ const computers = consentableComputerOperations(input.computers ?? [], app.declaredCapabilities);
241
+ if (agents.length === 0 && computers.length === 0)
242
+ return openSettings();
243
+ const outcome = await dialog.ask({
244
+ appName: target.title,
245
+ agents,
246
+ computers,
247
+ // This trusted shell only renders for the signed-in owner;
248
+ // their Allow click here is the whole grant, since this host
249
+ // keeps no further per-App ceiling to persist.
250
+ allow: () => Promise.resolve(true),
251
+ });
252
+ return outcome === "customize" ? openSettings() : outcome;
253
+ }
254
+ : (target, input) => requestAppAccess(target, input, dialog));
255
+ }, [controls, data]);
256
+ const settings = (_jsxs(_Fragment, { children: [_jsx(RouterProvider, { router: controls.router }), _jsx(AppAccessConsent, { request: consent }), _jsx(DeferredSystemSettings, { location: location, sections: controls.sections, onNavigate: (next) => {
257
+ controls.navigate(next);
258
+ // Closing Settings may have changed roles, grants or forks.
259
+ if (next === null)
260
+ controls.changed();
261
+ }, apps: directory.data?.apps ?? [], appCount: directory.data?.apps.length ?? null, onAccepted: async () => {
262
+ await directory.refetch({ throwOnError: true });
263
+ controls.changed();
264
+ }, desktopSettings: controls.desktopSettings, desktopConsentReadyAt: controls.desktopConsentReadyAt(), onClosed: controls.settingsClosed, restoreFocus: restoreFocus, ports: data })] }));
265
+ return data.avatar === undefined ? (settings) : (_jsx(MemberAvatarSource, { read: data.avatar.read, children: settings }));
266
+ }
@@ -0,0 +1,7 @@
1
+ /** Keep host and isolated-workbench reads in distinct cache entries. */
2
+ export declare const tourtoisQueryKeys: {
3
+ hostSettings: (workspaceId: string) => readonly ["tourtois", "host-settings", string];
4
+ workbench: () => readonly ["tourtois", "workbench"];
5
+ workbenchComputers: (teamId: string) => readonly ["tourtois", "workbench-computers", string];
6
+ appDirectory: (teamId: string) => readonly ["tourtois", "workbench-apps", string];
7
+ };
@@ -0,0 +1,7 @@
1
+ /** Keep host and isolated-workbench reads in distinct cache entries. */
2
+ export const tourtoisQueryKeys = {
3
+ hostSettings: (workspaceId) => ["tourtois", "host-settings", workspaceId],
4
+ workbench: () => ["tourtois", "workbench"],
5
+ workbenchComputers: (teamId) => ["tourtois", "workbench-computers", teamId],
6
+ appDirectory: (teamId) => ["tourtois", "workbench-apps", teamId],
7
+ };
@@ -0,0 +1,16 @@
1
+ import type { ShiitakeAgent } from "@fungi.computer/shiitake/client";
2
+ export type SessionClient<TSession> = Pick<ShiitakeAgent, "listSessions" | "createSession"> & Readonly<{
3
+ close: () => Promise<void>;
4
+ session: (sessionId: string) => TSession;
5
+ }>;
6
+ export type SessionClientFactory<TSession> = (agentId: string) => SessionClient<TSession>;
7
+ /**
8
+ * Own one Team's client/session generation from effect setup through cleanup.
9
+ * The render waits for setup so React StrictMode can rehearse and replace the
10
+ * generation without handing a child a client that cleanup already closed.
11
+ */
12
+ export declare function useSessionRegistry<TSession>(teamId: string, createClient: SessionClientFactory<TSession>): {
13
+ ready: boolean;
14
+ sessionFor: (agentId: string, sessionId: string) => TSession;
15
+ selectSession: (agentId: string) => Promise<string>;
16
+ };