@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,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button } from "@fungi.computer/caps/components/button";
3
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
4
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "@fungi.computer/caps/components/dropdown-menu";
5
+ const emptyMenu = Object.freeze([]);
6
+ const noSubscription = () => () => undefined;
7
+ function outcomeMessage(outcome) {
8
+ if (outcome.status === "handled")
9
+ return null;
10
+ if (outcome.status === "unavailable")
11
+ return outcome.reason;
12
+ if (outcome.status === "failed")
13
+ return outcome.error.message;
14
+ if (outcome.status === "closed")
15
+ return "This App has closed.";
16
+ return "This App action is no longer available.";
17
+ }
18
+ /** Render the focused App's Whistle commands in the desktop's system bar. */
19
+ export function AppMenus({ host, container: popover, }) {
20
+ const menu = useSyncExternalStore(host?.subscribe ?? noSubscription, host?.menu ?? (() => emptyMenu), () => emptyMenu);
21
+ const [pending, setPending] = useState(null);
22
+ const [message, setMessage] = useState(null);
23
+ const invocation = useRef(null);
24
+ const categories = useMemo(() => {
25
+ const grouped = new Map();
26
+ for (const command of menu) {
27
+ // This menu invokes argument-free actions. Typed handoffs remain in the
28
+ // host graph for programmatic discovery, not as broken clickable rows.
29
+ if (command.action?.inputSchema !== undefined)
30
+ continue;
31
+ const commands = grouped.get(command.category) ?? [];
32
+ commands.push(command);
33
+ grouped.set(command.category, commands);
34
+ }
35
+ return [...grouped];
36
+ }, [menu]);
37
+ useEffect(() => {
38
+ invocation.current?.abort();
39
+ invocation.current = null;
40
+ setPending(null);
41
+ setMessage(null);
42
+ return () => {
43
+ invocation.current?.abort();
44
+ invocation.current = null;
45
+ };
46
+ }, []);
47
+ if (!host || categories.length === 0)
48
+ return null;
49
+ return (_jsxs(_Fragment, { children: [categories.map(([category, commands]) => (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { size: "menubar", variant: "ghost" }), children: category }), _jsx(DropdownMenuContent, { align: "start", finalFocus: false, container: popover, children: commands.map((command) => {
50
+ const unavailable = command.availability.status === "unavailable";
51
+ return (_jsx(DropdownMenuItem, { disabled: pending !== null || unavailable, title: unavailable
52
+ ? command.availability.reason
53
+ : command.description, onSelect: () => {
54
+ const controller = new AbortController();
55
+ invocation.current = controller;
56
+ setPending(command.commandId);
57
+ setMessage(null);
58
+ void host
59
+ .execute(command.commandId, {
60
+ origin: "menu",
61
+ signal: controller.signal,
62
+ })
63
+ .then((outcome) => {
64
+ if (invocation.current === controller)
65
+ setMessage(outcomeMessage(outcome));
66
+ })
67
+ .catch((cause) => {
68
+ console.error("App command failed", cause);
69
+ if (invocation.current === controller &&
70
+ !controller.signal.aborted)
71
+ setMessage("This App action failed.");
72
+ })
73
+ .finally(() => {
74
+ if (invocation.current !== controller)
75
+ return;
76
+ invocation.current = null;
77
+ setPending(null);
78
+ });
79
+ }, children: command.title }, command.commandId));
80
+ }) })] }, category))), message ? (_jsx("span", { className: "tourtois-system-command-status", role: "status", children: message })) : null] }));
81
+ }
@@ -0,0 +1,8 @@
1
+ import { type AppOpenInput } from "@fungi.computer/agar/apps";
2
+ export type AppOpenUrlSearch = AppOpenInput;
3
+ /** URLSearchParams owns decoding; Agar owns the bounded intent grammar. */
4
+ export declare function appOpenUrlSearch(search: string): AppOpenUrlSearch | null;
5
+ export declare function appOpenInputFromUrl(search: string): AppOpenInput | null;
6
+ /** Keep unrelated query fields while consuming or dropping a bad handoff. */
7
+ export declare function withoutAppOpenUrlSearch(search: string): string;
8
+ export declare function cleanInvalidAppOpenUrlSearch(search: string): string;
@@ -0,0 +1,63 @@
1
+ import { appOpenInputSchema, } from "@fungi.computer/agar/apps";
2
+ import * as Schema from "effect/Schema";
3
+ const handoffFields = ["open", "app", "intent", "text"];
4
+ const handoffBaseFields = handoffFields;
5
+ /** URLSearchParams owns decoding; Agar owns the bounded intent grammar. */
6
+ export function appOpenUrlSearch(search) {
7
+ const parameters = new URLSearchParams(search);
8
+ if (handoffFields.some((field) => parameters.getAll(field).length > 1) ||
9
+ parameters.getAll("mode").length > 1)
10
+ return null;
11
+ if (parameters.has("open") === parameters.has("app"))
12
+ return null;
13
+ const targetKey = parameters.has("app") ? "app" : "open";
14
+ const kind = parameters.get("intent");
15
+ const required = kind === "compose"
16
+ ? [targetKey, "intent", "text"]
17
+ : kind === "navigate"
18
+ ? [targetKey, "intent"]
19
+ : [];
20
+ if (required.length === 0 || required.some((field) => !parameters.has(field)))
21
+ return null;
22
+ let intent = null;
23
+ if (kind === "compose")
24
+ intent = { kind, text: parameters.get("text") ?? "" };
25
+ if (kind === "navigate") {
26
+ const params = [...parameters.entries()]
27
+ .filter(([key]) => !handoffBaseFields.includes(key) && key !== "mode")
28
+ .map(([key, value]) => ({ key, value }));
29
+ if (params.some(({ key }) => key.length > 64 || key.length === 0) ||
30
+ new Set(params.map(({ key }) => key)).size !== params.length)
31
+ return null;
32
+ intent = { kind, params };
33
+ }
34
+ const candidate = parameters.has("app")
35
+ ? { appId: parameters.get("app"), intent }
36
+ : { role: parameters.get("open"), intent };
37
+ return Schema.is(appOpenInputSchema)(candidate) ? candidate : null;
38
+ }
39
+ export function appOpenInputFromUrl(search) {
40
+ return appOpenUrlSearch(search);
41
+ }
42
+ /** Keep unrelated query fields while consuming or dropping a bad handoff. */
43
+ export function withoutAppOpenUrlSearch(search) {
44
+ const parameters = new URLSearchParams(search);
45
+ const isNavigation = parameters.get("intent") === "navigate";
46
+ for (const field of handoffFields)
47
+ parameters.delete(field);
48
+ if (isNavigation) {
49
+ const remainingKeys = new Set(["mode"]);
50
+ for (const key of Array.from(parameters.keys()))
51
+ if (!remainingKeys.has(key))
52
+ parameters.delete(key);
53
+ }
54
+ const remaining = parameters.toString();
55
+ return remaining ? `?${remaining}` : "";
56
+ }
57
+ export function cleanInvalidAppOpenUrlSearch(search) {
58
+ const parameters = new URLSearchParams(search);
59
+ return handoffFields.some((field) => parameters.has(field)) &&
60
+ appOpenUrlSearch(search) === null
61
+ ? withoutAppOpenUrlSearch(search)
62
+ : search;
63
+ }
@@ -0,0 +1,62 @@
1
+ import { type ReactNode } from "react";
2
+ import type { WhistleCommand, WhistleExecutionInput, WhistleExecutionOutcome, WhistleMenuProjection, WhistleSnapshot } from "@fungi.computer/whistle";
3
+ import type { AgentSessionSelection, AppNavigationState, AppStatusItem, FileNavigationInput } from "@fungi.computer/agar";
4
+ import type { AppOpenInput, AppRoleChoice } from "@fungi.computer/agar/apps";
5
+ import type { AppDeclaredCommand } from "@fungi.computer/puffball/app";
6
+ import type { AppWindowMount } from "./app-windows.js";
7
+ /** What one mounted App may raise toward the shell. */
8
+ export type TourtoisAppMountEvents = Readonly<{
9
+ onComputerChoice?: () => Promise<void>;
10
+ onAgentSessionSelection: (selection: AgentSessionSelection | null) => Promise<void>;
11
+ /** A presentation-neutral file handoff to another App. */
12
+ onNavigation?: (input: FileNavigationInput) => Promise<void>;
13
+ onAppOpen?: (input: AppOpenInput) => Promise<void>;
14
+ /** Which App the Team's role routing would open for one role. */
15
+ onAppRole?: (role: string) => Promise<AppRoleChoice>;
16
+ onSelectionChanged?: () => Promise<boolean>;
17
+ /** The App's parsed navigation state for its window chrome. */
18
+ onNavigationState?: (state: AppNavigationState | null) => void;
19
+ /** The App's parsed status readouts for its window's status bar. */
20
+ onStatusItems?: (items: readonly AppStatusItem[]) => void;
21
+ /**
22
+ * The App's own document took focus, as when the person clicks into it.
23
+ * Its sandboxed frame keeps that pointer from this document, so the App's
24
+ * focus report is what raises its window.
25
+ */
26
+ onFocus?: () => void;
27
+ }>;
28
+ export type TourtoisAppSurfaceHost = Readonly<{
29
+ /** Mount an App and route its presentation-neutral events here. */
30
+ mount: (container: Parameters<AppWindowMount>[0], launch: Parameters<AppWindowMount>[1], signal: Parameters<AppWindowMount>[2], events: TourtoisAppMountEvents) => ReturnType<AppWindowMount>;
31
+ /** Select the App whose commands are currently addressable by the shell. */
32
+ focus?: (surfaceId: string | null) => void;
33
+ /** Register Tourtois's global shell commands in the shared Whistle graph. */
34
+ registerShellCommands?: (commands: readonly WhistleCommand<never>[]) => () => void;
35
+ /** Project Release-advertised rows into Cmd-K before the App is mounted. */
36
+ registerDirectoryCommands?: (commands: readonly Readonly<{
37
+ surfaceId: string;
38
+ appTitle: string;
39
+ command: AppDeclaredCommand;
40
+ }>[], run: (surfaceId: string, commandId: string) => Promise<void>) => () => void;
41
+ /** Commands contributed by the currently focused App window. */
42
+ menu: () => readonly WhistleMenuProjection[];
43
+ /** The shell-owned semantic command graph, including every mounted App. */
44
+ snapshot: () => WhistleSnapshot;
45
+ /**
46
+ * Commands active for the current focus: shell rows plus the focused App's
47
+ * rows. Palette, keys and the command line read this, never the whole graph.
48
+ */
49
+ active: () => WhistleSnapshot;
50
+ /** Execute an active command or an exact host-owned programmatic action. */
51
+ execute: (commandId: string, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
52
+ /** Bind one mounted App's explicit Agent Session selection to the shell. */
53
+ reportAgentSessionSelection?: (teamId: string, mountId: string, selection: AgentSessionSelection | null) => Promise<void> | void;
54
+ /** Observe focused App and command-graph changes. */
55
+ subscribe: (listener: () => void) => () => void;
56
+ }>;
57
+ export declare function AppSurfaceHostProvider({ children, value, }: {
58
+ children: ReactNode;
59
+ value: TourtoisAppSurfaceHost | null;
60
+ }): import("react/jsx-runtime").JSX.Element;
61
+ /** The outer authenticated host supplies the isolated App transport. */
62
+ export declare function useAppSurfaceHost(): TourtoisAppSurfaceHost | null;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const AppSurfaceHostContext = createContext(null);
4
+ export function AppSurfaceHostProvider({ children, value, }) {
5
+ return (_jsx(AppSurfaceHostContext.Provider, { value: value, children: children }));
6
+ }
7
+ /** The outer authenticated host supplies the isolated App transport. */
8
+ export function useAppSurfaceHost() {
9
+ return useContext(AppSurfaceHostContext);
10
+ }
@@ -0,0 +1,107 @@
1
+ import type { AppOpenRequest } from "@fungi.computer/agar";
2
+ import { type WindowHost, type WindowEvents, type WindowProjection, type WindowMount, type WindowManager, type WindowManagerOptions } from "@fungi.computer/oyster/shell/window-manager";
3
+ export type AppLaunchDescriptor = Readonly<{
4
+ windowId: string;
5
+ /** Stable presentation identity across default inheritance/customization. */
6
+ surfaceId: string;
7
+ installationId: string | null;
8
+ appId: string;
9
+ releaseId: string;
10
+ title: string;
11
+ href: string;
12
+ openRequest: AppOpenRequest | null;
13
+ /** Roles the App's Release declares; the shell's mark follows them. */
14
+ roles: readonly string[];
15
+ /** Digest-covered command declarations shown before the App is opened. */
16
+ commands: readonly import("@fungi.computer/puffball/app").AppDeclaredCommand[];
17
+ /**
18
+ * The granted Computers this window's status bar offers the person.
19
+ * Absent for an App that attaches to no Computer, or while the Team's
20
+ * Computers cannot be read.
21
+ */
22
+ computers?: readonly Readonly<{
23
+ computerId: string;
24
+ name: string;
25
+ }>[];
26
+ }>;
27
+ export type AppWindowHost = WindowHost<AppLaunchDescriptor>;
28
+ export type AppWindowEvents = WindowEvents;
29
+ export type AppWindowProjection = WindowProjection<AppLaunchDescriptor, HTMLElement>;
30
+ export type AppWindowMount = WindowMount<AppLaunchDescriptor, HTMLElement>;
31
+ export type AppWindowManager = WindowManager<AppLaunchDescriptor, HTMLElement>;
32
+ export type AppWindowManagerOptions = Pick<WindowManagerOptions<AppLaunchDescriptor, HTMLElement>, "mount" | "waitForAttachment"> & Readonly<{
33
+ createContainer?: (launch: AppLaunchDescriptor) => HTMLElement;
34
+ }>;
35
+ export declare const hasVisibleAppWindows: (projection: Readonly<{
36
+ windows: readonly Readonly<{
37
+ windowId: string;
38
+ launch: Readonly<{
39
+ windowId: string;
40
+ /** Stable presentation identity across default inheritance/customization. */
41
+ surfaceId: string;
42
+ installationId: string | null;
43
+ appId: string;
44
+ releaseId: string;
45
+ title: string;
46
+ href: string;
47
+ openRequest: AppOpenRequest | null;
48
+ /** Roles the App's Release declares; the shell's mark follows them. */
49
+ roles: readonly string[];
50
+ /** Digest-covered command declarations shown before the App is opened. */
51
+ commands: readonly Readonly<{
52
+ id: string;
53
+ title: string;
54
+ category: string;
55
+ description?: string | undefined;
56
+ sigil?: string | undefined;
57
+ projections?: Readonly<{
58
+ bindings?: (Readonly<{
59
+ order?: number | undefined;
60
+ }> & Readonly<{
61
+ values: readonly string[];
62
+ }>) | undefined;
63
+ agent?: (Readonly<{
64
+ order?: number | undefined;
65
+ }> & Readonly<{
66
+ keybind?: string | undefined;
67
+ }>) | undefined;
68
+ help?: Readonly<{
69
+ order?: number | undefined;
70
+ }> | undefined;
71
+ menu?: Readonly<{
72
+ order?: number | undefined;
73
+ }> | undefined;
74
+ palette?: Readonly<{
75
+ order?: number | undefined;
76
+ }> | undefined;
77
+ slash?: (Readonly<{
78
+ order?: number | undefined;
79
+ }> & Readonly<{
80
+ aliases: readonly string[];
81
+ }>) | undefined;
82
+ }> | undefined;
83
+ }>[];
84
+ /**
85
+ * The granted Computers this window's status bar offers the person.
86
+ * Absent for an App that attaches to no Computer, or while the Team's
87
+ * Computers cannot be read.
88
+ */
89
+ computers?: readonly Readonly<{
90
+ computerId: string;
91
+ name: string;
92
+ }>[];
93
+ }>;
94
+ container: HTMLElement;
95
+ status: import("@fungi.computer/oyster/shell/window-manager").WindowStatus;
96
+ generation: number;
97
+ error?: unknown;
98
+ cleanupError?: unknown;
99
+ navigation: Readonly<{
100
+ canGoBack: boolean;
101
+ title?: string;
102
+ }> | null;
103
+ statusItems: readonly import("@fungi.computer/agar").AppStatusItem[];
104
+ }>[];
105
+ }>) => boolean;
106
+ /** DOM attachment and App identity belong to the browser host. */
107
+ export declare function createAppWindowManager(options: AppWindowManagerOptions): AppWindowManager;
@@ -0,0 +1,21 @@
1
+ import { createWindowManager, hasVisibleWindows, } from "@fungi.computer/oyster/shell/window-manager";
2
+ export const hasVisibleAppWindows = (hasVisibleWindows);
3
+ /** DOM attachment and App identity belong to the browser host. */
4
+ export function createAppWindowManager(options) {
5
+ return createWindowManager({
6
+ ...options,
7
+ createContainer: options.createContainer ?? (() => document.createElement("div")),
8
+ isAttached: (container) => container.isConnected,
9
+ // The offered Computers are the shell's status bar alone; the App never
10
+ // receives them, so a new offer re-admits nothing.
11
+ isChromeOnlyChange: (current, next) => JSON.stringify(current.computers) !== JSON.stringify(next.computers) &&
12
+ JSON.stringify({ ...current, computers: null }) ===
13
+ JSON.stringify({ ...next, computers: null }),
14
+ validateReconfiguration: (current, next) => {
15
+ if (next.appId !== current.appId ||
16
+ next.surfaceId !== current.surfaceId ||
17
+ next.releaseId !== current.releaseId)
18
+ throw new Error("App identity cannot change during window reconfiguration");
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,153 @@
1
+ /* Tourtois desktop chrome: menu bar, App menus, command dialogs and status. */
2
+ .tourtois-desktop-menu {
3
+ display: flex;
4
+ align-items: center;
5
+ box-sizing: border-box;
6
+ /* A 48px bar on the Stipe grid: 40px launcher and 32px menu triggers sit
7
+ on whole 4px and 8px insets. Its rules are inset shadows, so the 1px
8
+ lines never push the row off the grid. */
9
+ flex: 0 0 var(--spacing-3xl);
10
+ height: var(--spacing-3xl);
11
+ min-height: var(--spacing-3xl);
12
+ gap: var(--spacing-xs);
13
+ padding: 0 var(--spacing-sm);
14
+ background: var(--color-base-200);
15
+ color: var(--color-base-content);
16
+ box-shadow:
17
+ inset 0 1px 0 color-mix(in oklab, white 24%, transparent),
18
+ inset 0 -1px 0
19
+ color-mix(in oklab, var(--color-base-content) 14%, var(--color-base-200));
20
+ font-size: 12px;
21
+ }
22
+ .tourtois-focused-app {
23
+ min-width: 0;
24
+ overflow: hidden;
25
+ font-weight: 750;
26
+ line-height: var(--spacing-lg);
27
+ padding-inline: var(--spacing-xs);
28
+ text-overflow: ellipsis;
29
+ white-space: nowrap;
30
+ }
31
+
32
+ .tourtois-command-palette {
33
+ display: grid;
34
+ gap: var(--spacing-sm);
35
+ max-width: min(44rem, calc(100vw - 1rem));
36
+ width: min(44rem, calc(100vw - 1rem));
37
+ }
38
+ .tourtois-command-palette-heading {
39
+ align-items: center;
40
+ display: flex;
41
+ font-size: var(--text-lg);
42
+ font-weight: 650;
43
+ gap: var(--spacing-sm);
44
+ padding: var(--spacing-xs) var(--spacing-sm);
45
+ }
46
+ .tourtois-command-palette-heading svg {
47
+ height: 1.5rem;
48
+ width: 1.5rem;
49
+ }
50
+ .tourtois-command-palette-item {
51
+ align-items: center;
52
+ display: flex;
53
+ gap: var(--spacing-xs);
54
+ min-width: 0;
55
+ width: 100%;
56
+ }
57
+ .tourtois-command-palette-item-icon {
58
+ display: grid;
59
+ flex: 0 0 1.5rem;
60
+ height: 1.5rem;
61
+ place-items: center;
62
+ width: 1.5rem;
63
+ }
64
+ .tourtois-command-palette-item-icon svg {
65
+ height: 1.5rem;
66
+ width: 1.5rem;
67
+ }
68
+ .tourtois-command-palette-item-title {
69
+ flex: 1 1 auto;
70
+ min-width: 0;
71
+ overflow: hidden;
72
+ text-overflow: ellipsis;
73
+ white-space: nowrap;
74
+ }
75
+ .tourtois-command-palette-item small {
76
+ flex: 0 0 auto;
77
+ color: var(--stipe-fg-muted);
78
+ font-size: var(--text-xs);
79
+ white-space: nowrap;
80
+ }
81
+ .tourtois-command-palette-shortcut {
82
+ align-items: center;
83
+ display: flex;
84
+ flex: 0 0 auto;
85
+ gap: 0.2rem;
86
+ justify-content: flex-end;
87
+ margin-left: auto;
88
+ }
89
+ .tourtois-command-palette-footer {
90
+ align-items: center;
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ gap: var(--spacing-sm);
94
+ justify-content: space-between;
95
+ padding: var(--spacing-xs) var(--spacing-sm) 0;
96
+ }
97
+ .tourtois-command-palette-close-hint {
98
+ color: var(--stipe-fg-muted);
99
+ font-size: var(--text-xs);
100
+ }
101
+ .tourtois-command-palette-leader-hint {
102
+ align-items: center;
103
+ display: flex;
104
+ flex-wrap: wrap;
105
+ gap: 0.2rem;
106
+ justify-content: flex-end;
107
+ }
108
+ @media (max-width: 28rem) {
109
+ .tourtois-command-palette-item small {
110
+ font-size: 0.6rem;
111
+ max-width: 4.5rem;
112
+ text-align: right;
113
+ white-space: normal;
114
+ }
115
+ .tourtois-command-palette-footer {
116
+ align-items: flex-start;
117
+ flex-direction: column;
118
+ }
119
+ .tourtois-command-palette-leader-hint {
120
+ justify-content: flex-start;
121
+ }
122
+ }
123
+
124
+ @media (max-width: 540px) {
125
+ .tourtois-desktop-menu {
126
+ gap: 0;
127
+ }
128
+ /* The App surface identifies itself without a clipped repeat in the bar. */
129
+ .tourtois-focused-app {
130
+ display: none;
131
+ }
132
+ }
133
+
134
+ /* App status actions: flat chips on the picker's surface that keep their
135
+ size, show a pressed or open state, and go icon-only on a phone. */
136
+ .tourtois-status-chip {
137
+ flex: none;
138
+ max-width: none;
139
+ }
140
+ .tourtois-status-chip[aria-pressed="true"],
141
+ .tourtois-status-chip[aria-expanded="true"] {
142
+ background: var(--color-base-300);
143
+ }
144
+ @media (max-width: 640px) {
145
+ .tourtois-status-chip:has(> svg) .tourtois-status-chip-label {
146
+ position: absolute;
147
+ width: 1px;
148
+ height: 1px;
149
+ overflow: hidden;
150
+ clip-path: inset(50%);
151
+ white-space: nowrap;
152
+ }
153
+ }
@@ -0,0 +1,42 @@
1
+ import { type ReactNode } from "react";
2
+ import type { WhistleSnapshot } from "@fungi.computer/whistle";
3
+ import type { AppDeclaredCommand } from "@fungi.computer/puffball/app";
4
+ import { type AppSurfaceCommandHost } from "@fungi.computer/oyster/shell/command-host";
5
+ /** Current command view and the host-owned dialog visibility. */
6
+ export type ShellCommandState = Readonly<{
7
+ active: WhistleSnapshot;
8
+ keysOpen: boolean;
9
+ /** Open or close the leader-key dialog in the owning shell. */
10
+ setKeysOpen: (open: boolean) => void;
11
+ commandLineOpen: boolean;
12
+ /** Open or close the command palette in the owning shell. */
13
+ setCommandLineOpen: (open: boolean) => void;
14
+ }>;
15
+ /** One installed App advertised in the shell command palette. */
16
+ export type PaletteApp = Readonly<{
17
+ commands: readonly AppDeclaredCommand[];
18
+ name: string;
19
+ icon?: string | null | undefined;
20
+ roles: readonly string[];
21
+ surfaceId: string;
22
+ }>;
23
+ /** Which-key list and `:` line for the focused window, drawn from Caps. */
24
+ export declare function CommandDialogs({ host, state, focusedWindow, directoryApps, onStatus, finalFocus, container: dialog, renderIcon, }: Readonly<{
25
+ host: Pick<AppSurfaceCommandHost, "execute"> | null;
26
+ state: ShellCommandState;
27
+ focusedWindow: Readonly<{
28
+ launch: Readonly<{
29
+ appId: string;
30
+ icon?: string | null | undefined;
31
+ commands: readonly AppDeclaredCommand[];
32
+ roles: readonly string[];
33
+ surfaceId: string;
34
+ title: string;
35
+ }>;
36
+ }> | undefined;
37
+ directoryApps: readonly PaletteApp[];
38
+ onStatus: (message: string) => void;
39
+ finalFocus: () => HTMLElement | null;
40
+ container: HTMLElement | null;
41
+ renderIcon: (roles: readonly string[], sigil?: string) => ReactNode;
42
+ }>): React.JSX.Element;