@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.
- package/LICENSE +21 -0
- package/README.md +127 -2
- package/dist/AgentMcpServers.d.ts +34 -0
- package/dist/AgentMcpServers.js +124 -0
- package/dist/AppAccessConsent.d.ts +23 -0
- package/dist/AppAccessConsent.js +122 -0
- package/dist/AppCallPermissions.d.ts +9 -0
- package/dist/AppCallPermissions.js +21 -0
- package/dist/AppDesktopSettings.d.ts +11 -0
- package/dist/AppDesktopSettings.js +102 -0
- package/dist/AppPane.d.ts +37 -0
- package/dist/AppPane.js +219 -0
- package/dist/AppWorkbench.d.ts +44 -0
- package/dist/AppWorkbench.js +575 -0
- package/dist/AvatarPicker.d.ts +7 -0
- package/dist/AvatarPicker.js +78 -0
- package/dist/AvatarPreview.d.ts +10 -0
- package/dist/AvatarPreview.js +6 -0
- package/dist/DesktopMenuBar.d.ts +18 -0
- package/dist/DesktopMenuBar.js +23 -0
- package/dist/FocusedAppMenus.d.ts +2 -0
- package/dist/FocusedAppMenus.js +10 -0
- package/dist/Keys.d.ts +131 -0
- package/dist/Keys.js +201 -0
- package/dist/MushroomMark.d.ts +2 -0
- package/dist/MushroomMark.js +5 -0
- package/dist/SettingsHome.d.ts +18 -0
- package/dist/SettingsHome.js +48 -0
- package/dist/SettingsNavigation.d.ts +17 -0
- package/dist/SettingsNavigation.js +153 -0
- package/dist/SystemSettings.d.ts +24 -0
- package/dist/SystemSettings.js +177 -0
- package/dist/TeamDesktop.d.ts +36 -0
- package/dist/TeamDesktop.js +39 -0
- package/dist/WorkbenchAppBar.d.ts +5 -0
- package/dist/WorkbenchAppBar.js +12 -0
- package/dist/app-authority.d.ts +150 -0
- package/dist/app-authority.js +41 -0
- package/dist/app-directory-reconciliation.d.ts +14 -0
- package/dist/app-directory-reconciliation.js +72 -0
- package/dist/app-directory.d.ts +206 -0
- package/dist/app-directory.js +21 -0
- package/dist/app-icon.d.ts +9 -0
- package/dist/app-icon.js +27 -0
- package/dist/app-launch.d.ts +53 -0
- package/dist/app-launch.js +204 -0
- package/dist/app-menus.d.ts +6 -0
- package/dist/app-menus.js +81 -0
- package/dist/app-open-url.d.ts +8 -0
- package/dist/app-open-url.js +63 -0
- package/dist/app-surface.d.ts +62 -0
- package/dist/app-surface.js +10 -0
- package/dist/app-windows.d.ts +134 -0
- package/dist/app-windows.js +21 -0
- package/dist/chrome.css +153 -0
- package/dist/command-dialogs.d.ts +42 -0
- package/dist/command-dialogs.js +194 -0
- package/dist/desktop/Desktop.d.ts +34 -0
- package/dist/desktop/Desktop.js +275 -0
- package/dist/desktop/consent.d.ts +4 -0
- package/dist/desktop/consent.js +64 -0
- package/dist/desktop/gill-frame.d.ts +14 -0
- package/dist/desktop/gill-frame.js +41 -0
- package/dist/desktop/gill-param-picker.d.ts +15 -0
- package/dist/desktop/gill-param-picker.js +145 -0
- package/dist/desktop/launcher-gill.d.ts +20 -0
- package/dist/desktop/launcher-gill.js +49 -0
- package/dist/desktop/own-launchers.d.ts +9 -0
- package/dist/desktop/own-launchers.js +9 -0
- package/dist/desktop/row-gill.d.ts +24 -0
- package/dist/desktop/row-gill.js +136 -0
- package/dist/desktop-api.d.ts +63 -0
- package/dist/desktop-api.js +31 -0
- package/dist/desktop-client.d.ts +19 -0
- package/dist/desktop-client.js +127 -0
- package/dist/desktop-layout.d.ts +117 -0
- package/dist/desktop-layout.js +305 -0
- package/dist/desktop-visibility.d.ts +8 -0
- package/dist/desktop-visibility.js +5 -0
- package/dist/layers.d.ts +5 -0
- package/dist/layers.js +31 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +17 -0
- package/dist/member-avatar-cache.d.ts +15 -0
- package/dist/member-avatar-cache.js +69 -0
- package/dist/member-avatar.d.ts +39 -0
- package/dist/member-avatar.js +88 -0
- package/dist/menu-bar.d.ts +5 -0
- package/dist/menu-bar.js +5 -0
- package/dist/model.d.ts +6 -0
- package/dist/model.js +13 -0
- package/dist/oyster-platform-controls.d.ts +4 -0
- package/dist/oyster-platform-controls.js +107 -0
- package/dist/platform-controls.d.ts +84 -0
- package/dist/platform-controls.js +266 -0
- package/dist/queries.d.ts +7 -0
- package/dist/queries.js +7 -0
- package/dist/session-registry.d.ts +16 -0
- package/dist/session-registry.js +69 -0
- package/dist/session-selection.d.ts +6 -0
- package/dist/session-selection.js +22 -0
- package/dist/settings-leave-guard.d.ts +5 -0
- package/dist/settings-leave-guard.js +9 -0
- package/dist/settings-navigation.css +129 -0
- package/dist/settings-routes.d.ts +118 -0
- package/dist/settings-routes.js +216 -0
- package/dist/settings-sections.d.ts +74 -0
- package/dist/settings-sections.js +17 -0
- package/dist/shell-commands.d.ts +15 -0
- package/dist/shell-commands.js +95 -0
- package/dist/shell-host.d.ts +52 -0
- package/dist/shell-host.js +288 -0
- package/dist/shell-module.d.ts +12 -0
- package/dist/shell-module.js +17 -0
- package/dist/shell-release.d.ts +3 -0
- package/dist/shell-release.js +4 -0
- package/dist/shell.css +858 -0
- package/dist/shell.d.ts +6 -0
- package/dist/shell.js +83 -0
- package/dist/spa.css +11 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +51 -0
- package/dist/team-directory.d.ts +3 -0
- package/dist/team-directory.js +6 -0
- package/dist/use-window-motion.d.ts +3 -0
- package/dist/use-window-motion.js +37 -0
- package/dist/useAppOpenUrlHandoff.d.ts +11 -0
- package/dist/useAppOpenUrlHandoff.js +62 -0
- package/dist/useAppWindowManager.d.ts +351 -0
- package/dist/useAppWindowManager.js +47 -0
- package/dist/useDirectoryCommands.d.ts +12 -0
- package/dist/useDirectoryCommands.js +80 -0
- package/dist/workbench.d.ts +3 -0
- package/dist/workbench.js +2 -0
- package/package.json +222 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jamstack Consulting LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tourtois (`@fungi.computer/tourtois`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tourtois is the desktop for your Apps: a launcher, tiled or floating windows,
|
|
4
|
+
menus and settings.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
Tourtois is the shell of [Oyster](https://github.com/fungi-computer/oyster), the
|
|
9
|
+
open App host that runs on your own Cloudflare account. When you sign in to an
|
|
10
|
+
Oyster, Tourtois is what you see, and every other App opens inside it.
|
|
11
|
+
|
|
12
|
+
## What you can do
|
|
13
|
+
|
|
14
|
+
- Launch Apps from the **Launcher** on the desktop or from the top bar.
|
|
15
|
+
- Arrange windows **Tiled** or **Floating** from the **Layout** menu: drag,
|
|
16
|
+
resize, zoom one window, minimize it and bring it back. A minimized App keeps
|
|
17
|
+
running, so restoring it never reloads it.
|
|
18
|
+
- Use the focused App's own menus in the top bar.
|
|
19
|
+
- Keep small live views of Apps on the desktop, such as Grove's inbox, and
|
|
20
|
+
**Arrange desktop** to add, move, resize and remove them.
|
|
21
|
+
- Open **System Settings…** from the mushroom menu for App permissions, keys and
|
|
22
|
+
agent connectors.
|
|
23
|
+
- On a phone, Apps open as a full-screen sheet over the desktop, and **Show
|
|
24
|
+
desktop** brings it back without losing unsaved work.
|
|
25
|
+
|
|
26
|
+
<img alt="Tourtois on a phone in the light theme: Grove's inbox above the Launcher" src="docs/media/desktop-phone.png" width="300">
|
|
27
|
+
|
|
28
|
+
Tourtois is a trusted shell. Every other App runs in its own sandboxed,
|
|
29
|
+
cross-origin frame, but Tourtois runs in the host's own page with your
|
|
30
|
+
permissions there. Pick a shell you trust: it can do anything you can on that
|
|
31
|
+
host.
|
|
32
|
+
|
|
33
|
+
## Get it
|
|
34
|
+
|
|
35
|
+
Tourtois installs with the other starter Apps, first of all. On a deployed
|
|
36
|
+
Oyster (see the
|
|
37
|
+
[Oyster quick start](https://github.com/fungi-computer/oyster#quick-start-deploy-your-own-oyster)),
|
|
38
|
+
run:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
export OYSTER_URL=https://oyster.<your-subdomain>.workers.dev
|
|
42
|
+
export OYSTER_OWNER_KEY=<your owner key>
|
|
43
|
+
npx oyster install-apps
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The installer clones this repository and the six others, seeds each on your
|
|
47
|
+
Oyster with `git push`, builds them on your Computer and installs them. Sign in
|
|
48
|
+
at `$OYSTER_URL/login` with the owner key and your Oyster opens on Tourtois. Run
|
|
49
|
+
`install-apps` again to update; each App keeps its data.
|
|
50
|
+
|
|
51
|
+
## Change it
|
|
52
|
+
|
|
53
|
+
Every installed App is also a template you can fork on your own Oyster,
|
|
54
|
+
including the desktop. Fork Tourtois into a Bonsai repository and clone it with
|
|
55
|
+
ordinary `git`:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
curl -X POST -H "Authorization: Bearer $OYSTER_OWNER_KEY" -H 'content-type: application/json' \
|
|
59
|
+
-d '{"appId":"app_tourtois","name":"my-tourtois"}' \
|
|
60
|
+
$OYSTER_URL/apps/bonsai/methods/apps.fork
|
|
61
|
+
git -c http.extraHeader="Authorization: Bearer $OYSTER_OWNER_KEY" \
|
|
62
|
+
clone $OYSTER_URL/git/bonsai/my-tourtois.git
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The reply names your fork's App id and repository. Push your edits, open the
|
|
66
|
+
repository in Bonsai and choose **Settings**, then **Build & install** with that
|
|
67
|
+
App id and your commit as `git:<sha>`. Install the Release at `tourtois` to
|
|
68
|
+
replace the desktop. Building alone never changes what you see; only the install
|
|
69
|
+
does.
|
|
70
|
+
|
|
71
|
+
To work from GitHub instead, fork this repository and point the installer at
|
|
72
|
+
your forks with `--repositories <base URL>` and `--ref <branch or tag>`. The
|
|
73
|
+
installer reads all seven starter Apps from that base, so fork them under one
|
|
74
|
+
account.
|
|
75
|
+
|
|
76
|
+
## How a host mounts it
|
|
77
|
+
|
|
78
|
+
A Tourtois Release ships `shell.js` beside its `index.html`. The host imports
|
|
79
|
+
that module from its own origin and calls
|
|
80
|
+
`mountShell(root, { workbench, storage, methods })`, which renders the desktop
|
|
81
|
+
into the host's page with the host's workbench, Tourtois's own storage and its
|
|
82
|
+
methods. Tourtois's code loads as one lazy chunk behind that module, so no host
|
|
83
|
+
bundles it, and a fork mounts the same way.
|
|
84
|
+
|
|
85
|
+
Tourtois asks the host to mount each App in that App's own sandboxed frame,
|
|
86
|
+
under its own installation, Release and permissions. Settings pages are supplied
|
|
87
|
+
by the host too: Tourtois offers a page only when the host backs it.
|
|
88
|
+
|
|
89
|
+
## Agent tools
|
|
90
|
+
|
|
91
|
+
Agents can arrange your desktop. Through Oyster's MCP endpoint, or at
|
|
92
|
+
`POST $OYSTER_URL/apps/tourtois/methods/<name>` with a key (see
|
|
93
|
+
[Git, keys and MCP](https://github.com/fungi-computer/oyster#git-keys-and-mcp)),
|
|
94
|
+
they call:
|
|
95
|
+
|
|
96
|
+
| Method | What it does |
|
|
97
|
+
| ------------------------------------------------------------------ | --------------------------------------------------------------- |
|
|
98
|
+
| `layout.get` / `widgets.available` | Reads the desktop and the live views Apps offer. |
|
|
99
|
+
| `layout.place` / `layout.move` / `layout.resize` / `layout.remove` | Changes one item on the desktop. |
|
|
100
|
+
| `layout.launcher` / `layout.set` / `layout.reset` | Pins or hides launcher Apps, replaces the layout, or resets it. |
|
|
101
|
+
|
|
102
|
+
Each change names the revision it is based on, and a stale revision is refused
|
|
103
|
+
rather than overwriting a newer arrangement. In a connected browser, an agent's
|
|
104
|
+
change shows **Your Agent arranged the desktop** with **Undo** for ten seconds.
|
|
105
|
+
|
|
106
|
+
## Build and check
|
|
107
|
+
|
|
108
|
+
Tourtois is React and TypeScript. Windows and tiling come from
|
|
109
|
+
[Maosaico](https://github.com/fungi-computer/maosaico) and the controls from
|
|
110
|
+
[Caps](https://github.com/fungi-computer/caps).
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
pnpm install
|
|
114
|
+
pnpm build
|
|
115
|
+
pnpm typecheck
|
|
116
|
+
pnpm test
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
[src/AppWorkbench.tsx](src/AppWorkbench.tsx) composes the App list, windows and
|
|
120
|
+
layout. [src/shell-module.ts](src/shell-module.ts) is the `mountShell` entry,
|
|
121
|
+
[src/platform-controls.tsx](src/platform-controls.tsx) the Settings window, and
|
|
122
|
+
[src/settings-sections.ts](src/settings-sections.ts) the Settings pages a host
|
|
123
|
+
can supply. The agent tools are in [backend/methods.ts](backend/methods.ts), and
|
|
124
|
+
[package.json](package.json) declares what the shell asks of its host.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** Every Agent with its connected MCP servers. */
|
|
3
|
+
export declare const agentMcpServersViewSchema: z.ZodObject<{
|
|
4
|
+
agents: z.ZodArray<z.ZodObject<{
|
|
5
|
+
agentId: z.ZodString;
|
|
6
|
+
name: z.ZodString;
|
|
7
|
+
servers: z.ZodArray<z.ZodObject<{
|
|
8
|
+
serverId: z.ZodString;
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
url: z.ZodString;
|
|
11
|
+
hasBearer: z.ZodBoolean;
|
|
12
|
+
}, z.core.$strict>>;
|
|
13
|
+
}, z.core.$strict>>;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
export type AgentMcpServersView = z.output<typeof agentMcpServersViewSchema>;
|
|
16
|
+
/** What the owner submits to connect one server. */
|
|
17
|
+
export type AgentMcpServerDraft = Readonly<{
|
|
18
|
+
name: string;
|
|
19
|
+
url: string;
|
|
20
|
+
bearer: string | null;
|
|
21
|
+
}>;
|
|
22
|
+
/** The host's owner-direct custody for Agent MCP servers. */
|
|
23
|
+
export type AgentMcpServersPort = Readonly<{
|
|
24
|
+
list(): Promise<AgentMcpServersView>;
|
|
25
|
+
add(agentId: string, draft: AgentMcpServerDraft): Promise<void>;
|
|
26
|
+
remove(agentId: string, serverId: string): Promise<void>;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* The Connectors settings on a standalone host: connect a remote MCP server to
|
|
30
|
+
* an Agent. Its tools become `env.mcp.<name>.<tool>()` in that Agent's code.
|
|
31
|
+
*/
|
|
32
|
+
export declare function AgentMcpServers({ port }: {
|
|
33
|
+
port: AgentMcpServersPort;
|
|
34
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { SectionHeading } from "@fungi.computer/caps/blocks/section-heading";
|
|
3
|
+
import { ActionRow } from "@fungi.computer/caps/components/action-row";
|
|
4
|
+
import { Badge } from "@fungi.computer/caps/components/badge";
|
|
5
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
6
|
+
import { DataList, DataListHeading, DataRow, } from "@fungi.computer/caps/components/data-list";
|
|
7
|
+
import { FormFooter } from "@fungi.computer/caps/components/field";
|
|
8
|
+
import { SchemaForm, field, useSchemaForm } from "@fungi.computer/caps/form";
|
|
9
|
+
import { Sigil } from "@fungi.computer/sigils";
|
|
10
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { Fragment, useMemo, useState } from "react";
|
|
12
|
+
import * as z from "zod";
|
|
13
|
+
/** One connected MCP server as the host lists it; the token never comes back. */
|
|
14
|
+
const agentMcpServerSchema = z.strictObject({
|
|
15
|
+
serverId: z.string().min(1),
|
|
16
|
+
name: z.string().min(1),
|
|
17
|
+
url: z.string().min(1),
|
|
18
|
+
hasBearer: z.boolean(),
|
|
19
|
+
});
|
|
20
|
+
/** Every Agent with its connected MCP servers. */
|
|
21
|
+
export const agentMcpServersViewSchema = z.strictObject({
|
|
22
|
+
agents: z.array(z.strictObject({
|
|
23
|
+
agentId: z.string().min(1),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
servers: z.array(agentMcpServerSchema),
|
|
26
|
+
})),
|
|
27
|
+
});
|
|
28
|
+
const queryKey = ["tourtois", "agent-mcp-servers"];
|
|
29
|
+
/**
|
|
30
|
+
* The Connectors settings on a standalone host: connect a remote MCP server to
|
|
31
|
+
* an Agent. Its tools become `env.mcp.<name>.<tool>()` in that Agent's code.
|
|
32
|
+
*/
|
|
33
|
+
export function AgentMcpServers({ port }) {
|
|
34
|
+
const client = useQueryClient();
|
|
35
|
+
const view = useQuery({ queryKey, queryFn: () => port.list() });
|
|
36
|
+
const refresh = () => client.invalidateQueries({ queryKey });
|
|
37
|
+
const remove = useMutation({
|
|
38
|
+
mutationFn: (input) => port.remove(input.agentId, input.serverId),
|
|
39
|
+
onSuccess: refresh,
|
|
40
|
+
});
|
|
41
|
+
const agents = view.data?.agents ?? [];
|
|
42
|
+
// A fresh form after each connected server, so the token field empties.
|
|
43
|
+
const [formKey, setFormKey] = useState(0);
|
|
44
|
+
return (_jsxs("section", { className: "tourtois-settings-content tourtois-connectors", "aria-label": "Connectors", children: [_jsx(SectionHeading, { children: "Connectors" }), _jsxs("p", { className: "tourtois-settings-lede", children: ["Connect a remote MCP server to an Agent. Its tools become", " ", _jsx("code", { children: "env.mcp.<name>" }), " in that Agent's code. A token stays with the Agent and is never shown again."] }), view.isPending ? _jsx("p", { children: "Loading MCP servers\u2026" }) : null, view.isError ? (_jsxs("div", { role: "status", children: [_jsx("p", { children: "MCP servers could not be loaded." }), _jsx(Button, { size: "sm", onClick: () => void view.refetch(), children: "Retry" })] })) : null, view.isSuccess && agents.length === 0 ? (_jsx("p", { children: "Create an Agent first, then connect MCP servers to it." })) : null, agents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DataList, { "aria-label": "Agent MCP servers", children: agents.map((agent) => (_jsxs(Fragment, { children: [_jsx(DataListHeading, { children: agent.name }), agent.servers.length === 0 ? (_jsx(DataRow, { children: "No MCP servers." })) : null, agent.servers.map((server) => (_jsx(DataRow, { className: "p-0", children: _jsx(ActionRow, { icon: _jsx(Sigil, { name: "connected" }), label: _jsxs(_Fragment, { children: [server.name, " ", _jsx(Badge, { size: "sm", variant: "quiet", children: "MCP" })] }), description: server.url, trailing: _jsxs(_Fragment, { children: [server.hasBearer ? (_jsx(Badge, { size: "sm", variant: "quiet", children: "Token saved" })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Remove ${server.name} from ${agent.name}`, disabled: remove.isPending, onClick: () => {
|
|
45
|
+
remove.mutate({
|
|
46
|
+
agentId: agent.agentId,
|
|
47
|
+
serverId: server.serverId,
|
|
48
|
+
});
|
|
49
|
+
}, children: "Remove" })] }) }) }, server.serverId)))] }, agent.agentId))) }), remove.isError ? (_jsx("p", { role: "alert", children: "The MCP server could not be removed." })) : null, _jsx(AddMcpServer, { agents: agents, onAdd: async (agentId, draft) => {
|
|
50
|
+
await port.add(agentId, draft);
|
|
51
|
+
await refresh();
|
|
52
|
+
setFormKey((current) => current + 1);
|
|
53
|
+
} }, formKey)] })) : null] }));
|
|
54
|
+
}
|
|
55
|
+
function AddMcpServer({ agents, onAdd, }) {
|
|
56
|
+
const schema = useMemo(() => z.object({
|
|
57
|
+
agentId: z.string().meta(field({
|
|
58
|
+
label: "Agent",
|
|
59
|
+
kind: "select",
|
|
60
|
+
options: Object.fromEntries(agents.map((agent) => [agent.agentId, agent.name])),
|
|
61
|
+
})),
|
|
62
|
+
name: z
|
|
63
|
+
.string()
|
|
64
|
+
.regex(/^[a-z][a-z0-9_]{0,31}$/u, "Lowercase letters, digits and _, starting with a letter")
|
|
65
|
+
.meta(field({
|
|
66
|
+
label: "Name",
|
|
67
|
+
help: "The Agent calls its tools as env.mcp.<name>.",
|
|
68
|
+
maxLength: 32,
|
|
69
|
+
placeholder: "linear",
|
|
70
|
+
})),
|
|
71
|
+
url: z
|
|
72
|
+
.string()
|
|
73
|
+
.max(2_048)
|
|
74
|
+
.refine((value) => URL.canParse(value) && new URL(value).protocol === "https:", "Use the server's https:// address")
|
|
75
|
+
.meta(field({
|
|
76
|
+
label: "Server URL",
|
|
77
|
+
kind: "url",
|
|
78
|
+
placeholder: "https://mcp.example.com/mcp",
|
|
79
|
+
})),
|
|
80
|
+
bearer: z
|
|
81
|
+
.string()
|
|
82
|
+
.max(4_096)
|
|
83
|
+
.meta(field({
|
|
84
|
+
label: "Bearer token",
|
|
85
|
+
help: "Only if the server asks for one.",
|
|
86
|
+
optional: true,
|
|
87
|
+
kind: "secret",
|
|
88
|
+
})),
|
|
89
|
+
}), [agents]);
|
|
90
|
+
const add = useMutation({
|
|
91
|
+
mutationFn: (input) => onAdd(input.agentId, input.draft),
|
|
92
|
+
});
|
|
93
|
+
const model = useSchemaForm({
|
|
94
|
+
schema,
|
|
95
|
+
defaultValues: {
|
|
96
|
+
agentId: agents[0]?.agentId ?? "",
|
|
97
|
+
name: "",
|
|
98
|
+
url: "",
|
|
99
|
+
bearer: "",
|
|
100
|
+
},
|
|
101
|
+
onSubmit: async (value) => {
|
|
102
|
+
try {
|
|
103
|
+
await add.mutateAsync({
|
|
104
|
+
agentId: value.agentId,
|
|
105
|
+
draft: {
|
|
106
|
+
name: value.name,
|
|
107
|
+
url: value.url,
|
|
108
|
+
bearer: value.bearer.length === 0 ? null : value.bearer,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return { kind: "saved" };
|
|
112
|
+
}
|
|
113
|
+
catch (cause) {
|
|
114
|
+
return {
|
|
115
|
+
kind: "refused",
|
|
116
|
+
error: cause instanceof Error
|
|
117
|
+
? cause.message
|
|
118
|
+
: "The MCP server could not be connected.",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return (_jsx(SchemaForm, { model: model, "aria-label": "Add MCP server", disabled: add.isPending, footer: _jsx(FormFooter, { error: model.error, children: _jsx(Button, { type: "submit", children: add.isPending ? "Connecting…" : "Add MCP server" }) }) }));
|
|
124
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentAccessOperation, AppPermissionOutcome, ComputerAccessOperation } from "@fungi.computer/agar";
|
|
2
|
+
import type { AppAgentPermission, AppComputerPermission } from "@fungi.computer/oyster/shell/app-directory";
|
|
3
|
+
export declare const consentableAgentOperations: (requested: readonly AgentAccessOperation[], declared: readonly string[]) => readonly AgentAccessOperation[];
|
|
4
|
+
export declare const consentableComputerOperations: (requested: readonly ComputerAccessOperation[], declared: readonly string[]) => readonly ComputerAccessOperation[];
|
|
5
|
+
/** The all-Team-Agents grant that adds `offered` to the current policy. */
|
|
6
|
+
export declare function consentedAgentPermission(current: AppAgentPermission, offered: readonly AgentAccessOperation[]): AppAgentPermission | "covered" | "customize";
|
|
7
|
+
/**
|
|
8
|
+
* The all-Team-Computers grant that adds `offered`. An empty selection is no
|
|
9
|
+
* access; a real Computer selection is the owner's choice and never widened.
|
|
10
|
+
*/
|
|
11
|
+
export declare function consentedComputerPermission(current: AppComputerPermission | null, offered: readonly ComputerAccessOperation[]): AppComputerPermission | "covered" | "customize";
|
|
12
|
+
export type AppAccessRequest = Readonly<{
|
|
13
|
+
appName: string;
|
|
14
|
+
agents: readonly AgentAccessOperation[];
|
|
15
|
+
computers: readonly ComputerAccessOperation[];
|
|
16
|
+
/** Save the grant through the owner's own authority; resolves false when it failed. */
|
|
17
|
+
allow: () => Promise<boolean>;
|
|
18
|
+
settle: (outcome: AppPermissionOutcome) => void;
|
|
19
|
+
}>;
|
|
20
|
+
/** One-click host consent for the rights a default App declared. */
|
|
21
|
+
export declare function AppAccessConsent({ request, }: {
|
|
22
|
+
request: AppAccessRequest | null;
|
|
23
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
3
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle, } from "@fungi.computer/caps/components/dialog";
|
|
4
|
+
import { WindowFrame, WindowHeader, } from "@fungi.computer/caps/components/window";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
const directoryOperations = ["agent.list"];
|
|
7
|
+
const sessionOperations = ["agent.read", "agent.send", "agent.manage"];
|
|
8
|
+
const agentOperations = [
|
|
9
|
+
"agent.list",
|
|
10
|
+
"agent.manage",
|
|
11
|
+
"agent.read",
|
|
12
|
+
"agent.send",
|
|
13
|
+
];
|
|
14
|
+
const computerOperations = [
|
|
15
|
+
"computer.list",
|
|
16
|
+
"computer.readFile",
|
|
17
|
+
"computer.writeFile",
|
|
18
|
+
"computer.execute",
|
|
19
|
+
];
|
|
20
|
+
/** What the trusted host may offer: the request bounded by the declared ceiling. */
|
|
21
|
+
function consentableOperations(known, requested, declared) {
|
|
22
|
+
return known.filter((operation) => requested.includes(operation) && declared.includes(operation));
|
|
23
|
+
}
|
|
24
|
+
export const consentableAgentOperations = (requested, declared) => consentableOperations(agentOperations, requested, declared);
|
|
25
|
+
export const consentableComputerOperations = (requested, declared) => consentableOperations(computerOperations, requested, declared);
|
|
26
|
+
/** The all-Team-Agents grant that adds `offered` to the current policy. */
|
|
27
|
+
export function consentedAgentPermission(current, offered) {
|
|
28
|
+
if (current.kind === "selected-agents")
|
|
29
|
+
return "customize";
|
|
30
|
+
const currentOperations = [
|
|
31
|
+
...current.directoryOperations,
|
|
32
|
+
...current.sessionOperations,
|
|
33
|
+
];
|
|
34
|
+
if (offered.every((operation) => currentOperations.includes(operation)))
|
|
35
|
+
return "covered";
|
|
36
|
+
const granted = new Set([...currentOperations, ...offered]);
|
|
37
|
+
return {
|
|
38
|
+
kind: "all-team-agents",
|
|
39
|
+
directoryOperations: directoryOperations.filter((operation) => granted.has(operation)),
|
|
40
|
+
sessionOperations: sessionOperations.filter((operation) => granted.has(operation)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The all-Team-Computers grant that adds `offered`. An empty selection is no
|
|
45
|
+
* access; a real Computer selection is the owner's choice and never widened.
|
|
46
|
+
*/
|
|
47
|
+
export function consentedComputerPermission(current, offered) {
|
|
48
|
+
if (current?.kind === "selected-computers" && current.computers.length > 0)
|
|
49
|
+
return "customize";
|
|
50
|
+
const currentOperations = current?.kind === "all-team-computers" ? current.operations : [];
|
|
51
|
+
if (offered.every((operation) => currentOperations.includes(operation)))
|
|
52
|
+
return "covered";
|
|
53
|
+
const granted = new Set([...currentOperations, ...offered]);
|
|
54
|
+
return {
|
|
55
|
+
kind: "all-team-computers",
|
|
56
|
+
operations: computerOperations.filter((operation) => granted.has(operation)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Sentence variants following "<App> can" in the consent description. */
|
|
60
|
+
const rightNames = {
|
|
61
|
+
"agent.list": "see your Agents",
|
|
62
|
+
"agent.manage": "create and manage Agents and their sessions",
|
|
63
|
+
"agent.read": "read their conversations",
|
|
64
|
+
"agent.send": "send messages",
|
|
65
|
+
"computer.list": "see your Computers",
|
|
66
|
+
"computer.readFile": "read their files",
|
|
67
|
+
"computer.writeFile": "change their files",
|
|
68
|
+
"computer.execute": "run commands on them",
|
|
69
|
+
};
|
|
70
|
+
const describe = (offered) => {
|
|
71
|
+
const rights = [...agentOperations, ...computerOperations]
|
|
72
|
+
.filter((operation) => offered.includes(operation))
|
|
73
|
+
.map((operation) => rightNames[operation]);
|
|
74
|
+
const last = rights.at(-1) ?? "";
|
|
75
|
+
return rights.length < 2
|
|
76
|
+
? last
|
|
77
|
+
: `${rights.slice(0, -1).join(", ")} and ${last}`;
|
|
78
|
+
};
|
|
79
|
+
const resources = (request, agent, computer) => [
|
|
80
|
+
request.agents.length > 0 ? agent : null,
|
|
81
|
+
request.computers.length > 0 ? computer : null,
|
|
82
|
+
]
|
|
83
|
+
.filter((resource) => resource !== null)
|
|
84
|
+
.join(" and ");
|
|
85
|
+
/** One-click host consent for the rights a default App declared. */
|
|
86
|
+
export function AppAccessConsent({ request, }) {
|
|
87
|
+
const [saving, setSaving] = useState(false);
|
|
88
|
+
const [failed, setFailed] = useState(false);
|
|
89
|
+
const settle = (outcome) => {
|
|
90
|
+
setSaving(false);
|
|
91
|
+
setFailed(false);
|
|
92
|
+
request?.settle(outcome);
|
|
93
|
+
};
|
|
94
|
+
return (_jsx(Dialog, { open: request !== null, onOpenChange: (next) => {
|
|
95
|
+
if (!next && !saving)
|
|
96
|
+
settle("declined");
|
|
97
|
+
}, children: _jsxs(DialogContent, { render: _jsx(WindowFrame, { floating: true }), keepMounted: false, className: "tourtois-app-consent", children: [_jsx(WindowHeader, { title: "App access" }), _jsxs("div", { className: "tourtois-app-consent-body", children: [_jsx(DialogTitle, { children: request
|
|
98
|
+
? `Let ${request.appName} work with your ${resources(request, "Agents", "Computers")}?`
|
|
99
|
+
: "" }), _jsx(DialogDescription, { children: request
|
|
100
|
+
? `${request.appName} can ${describe([...request.agents, ...request.computers])} for every ${resources(request, "Agent", "Computer")} in this Oyster. You can change this later in System Settings.`
|
|
101
|
+
: "" }), failed ? (_jsx("p", { role: "alert", children: "Access could not be saved. Try again." })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", disabled: saving, onClick: () => {
|
|
102
|
+
settle("customize");
|
|
103
|
+
}, children: "Customize\u2026" }), _jsx(Button, { variant: "ghost", disabled: saving, onClick: () => {
|
|
104
|
+
settle("declined");
|
|
105
|
+
}, children: "Not now" }), _jsx(Button, { disabled: saving || request === null, onClick: () => {
|
|
106
|
+
if (request === null)
|
|
107
|
+
return;
|
|
108
|
+
setSaving(true);
|
|
109
|
+
setFailed(false);
|
|
110
|
+
void request.allow().then((saved) => {
|
|
111
|
+
if (saved)
|
|
112
|
+
settle("granted");
|
|
113
|
+
else {
|
|
114
|
+
setSaving(false);
|
|
115
|
+
setFailed(true);
|
|
116
|
+
}
|
|
117
|
+
}, () => {
|
|
118
|
+
setSaving(false);
|
|
119
|
+
setFailed(true);
|
|
120
|
+
});
|
|
121
|
+
}, children: saving ? "Allowing…" : "Allow" })] })] })] }) }));
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AppDeclaration } from "@fungi.computer/puffball/app";
|
|
2
|
+
/** The declared deputy ceiling, shown identically on both hosts; this grants nothing. */
|
|
3
|
+
export declare function AppCallPermissions({ calls, apps, }: {
|
|
4
|
+
calls: AppDeclaration["calls"];
|
|
5
|
+
apps: readonly {
|
|
6
|
+
appId: string;
|
|
7
|
+
displayName?: string | undefined;
|
|
8
|
+
}[];
|
|
9
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DataList, DataListHeading, DataRow, } from "@fungi.computer/caps/components/data-list";
|
|
3
|
+
const methodLabels = new Map([
|
|
4
|
+
["files.tree", "browse files"],
|
|
5
|
+
["files.list", "list files"],
|
|
6
|
+
["files.read", "read files"],
|
|
7
|
+
["commits.list", "read commits"],
|
|
8
|
+
["rebuildCopies", "refresh file views"],
|
|
9
|
+
["files.save", "save files"],
|
|
10
|
+
["files.sync", "sync files"],
|
|
11
|
+
["files.discard", "discard edits"],
|
|
12
|
+
]);
|
|
13
|
+
/** The declared deputy ceiling, shown identically on both hosts; this grants nothing. */
|
|
14
|
+
export function AppCallPermissions({ calls, apps, }) {
|
|
15
|
+
if (!calls?.length)
|
|
16
|
+
return null;
|
|
17
|
+
return (_jsxs("section", { "aria-label": "Calls to other Apps", children: [_jsx(DataListHeading, { level: 3, children: "Other Apps" }), _jsx(DataList, { density: "compact", children: calls.map((call) => (_jsx(DataRow, { children: _jsxs("span", { children: ["Can use", " ", apps.find((app) => app.appId === call.appId)?.displayName ??
|
|
18
|
+
call.appId, ":", " ", call.methods
|
|
19
|
+
.map((method) => methodLabels.get(method) ?? method)
|
|
20
|
+
.join(", ")] }) }, call.appId))) }), _jsx("p", { className: "mt-1 text-xs", children: "Calls use your permissions in the other App. Renamed target methods need approval again." })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorkbenchDesktopSettings } from "@fungi.computer/oyster/shell/workbench";
|
|
2
|
+
import type { AppDirectoryEntry, AppDirectoryGill } from "@fungi.computer/oyster/shell/app-directory";
|
|
3
|
+
export type AppDesktopSettingsApp = Pick<AppDirectoryEntry, "surfaceId" | "availability"> & Readonly<{
|
|
4
|
+
gills: readonly AppDirectoryGill[];
|
|
5
|
+
}>;
|
|
6
|
+
/** Suggestions only: the member chooses, then the admitted dashboard arranges. */
|
|
7
|
+
export declare function AppDesktopSettings({ app, desktop, consentReadyAt, }: {
|
|
8
|
+
app: AppDesktopSettingsApp;
|
|
9
|
+
desktop: WorkbenchDesktopSettings;
|
|
10
|
+
consentReadyAt?: number;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DataListHeading } from "@fungi.computer/caps/components/data-list";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { AppError } from "@fungi.computer/agar";
|
|
5
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
6
|
+
import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
|
|
7
|
+
import { Select } from "@fungi.computer/caps/components/select";
|
|
8
|
+
const sizeLabels = { S: "Small", M: "Medium", L: "Large" };
|
|
9
|
+
function GillOption({ app, gill, desktop, consentReadyAt, }) {
|
|
10
|
+
const sizes = gill.desktop?.sizes ?? [];
|
|
11
|
+
const [size, setSize] = useState(sizes[0] ?? "S");
|
|
12
|
+
const [saved, setSaved] = useState(null);
|
|
13
|
+
const [pending, setPending] = useState(false);
|
|
14
|
+
const busy = useRef(false);
|
|
15
|
+
const [consentReady, setConsentReady] = useState(Date.now() >= consentReadyAt);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setConsentReady(Date.now() >= consentReadyAt);
|
|
18
|
+
const timer = window.setTimeout(() => {
|
|
19
|
+
setConsentReady(true);
|
|
20
|
+
}, Math.max(0, consentReadyAt - Date.now()));
|
|
21
|
+
return () => {
|
|
22
|
+
window.clearTimeout(timer);
|
|
23
|
+
};
|
|
24
|
+
}, [consentReadyAt]);
|
|
25
|
+
const [error, setError] = useState(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let retired = false;
|
|
28
|
+
setSaved(null);
|
|
29
|
+
void Promise.resolve()
|
|
30
|
+
.then(() => desktop.readGill({ surfaceId: app.surfaceId, gillId: gill.id }))
|
|
31
|
+
.then((value) => {
|
|
32
|
+
if (!retired)
|
|
33
|
+
setSaved(value);
|
|
34
|
+
})
|
|
35
|
+
.catch((cause) => {
|
|
36
|
+
console.error("Desktop Gill saved state could not be read", {
|
|
37
|
+
surfaceId: app.surfaceId,
|
|
38
|
+
gillId: gill.id,
|
|
39
|
+
cause,
|
|
40
|
+
});
|
|
41
|
+
if (!retired)
|
|
42
|
+
setError("Your desktop could not be read. Reopen Settings to try again.");
|
|
43
|
+
return { kind: "failed", cause };
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
retired = true;
|
|
47
|
+
};
|
|
48
|
+
}, [app.surfaceId, gill.id, desktop]);
|
|
49
|
+
const onDesktop = !!saved?.length;
|
|
50
|
+
const change = async () => {
|
|
51
|
+
if (busy.current || saved === null || Date.now() < consentReadyAt)
|
|
52
|
+
return { kind: "busy" };
|
|
53
|
+
busy.current = true;
|
|
54
|
+
setPending(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
const selection = { surfaceId: app.surfaceId, gillId: gill.id };
|
|
57
|
+
try {
|
|
58
|
+
if (onDesktop)
|
|
59
|
+
await desktop.removeGill(selection);
|
|
60
|
+
else
|
|
61
|
+
await desktop.addGill({ ...selection, size });
|
|
62
|
+
setSaved(await desktop.readGill(selection));
|
|
63
|
+
return { kind: "confirmed" };
|
|
64
|
+
}
|
|
65
|
+
catch (cause) {
|
|
66
|
+
console.error("Desktop Gill member change failed", {
|
|
67
|
+
...selection,
|
|
68
|
+
cause,
|
|
69
|
+
});
|
|
70
|
+
setError(cause instanceof AppError
|
|
71
|
+
? cause.message
|
|
72
|
+
: "The widget could not be changed. Reopen Settings to try again.");
|
|
73
|
+
// A refusal may mean another tab changed the saved layout. Never keep an optimistic membership flag.
|
|
74
|
+
setSaved(null);
|
|
75
|
+
return { kind: "failed", cause };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
busy.current = false;
|
|
79
|
+
setPending(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return (_jsxs(DataRow, { style: { gridTemplateColumns: "minmax(0, 1fr)" }, children: [_jsxs("div", { children: [_jsx("strong", { children: gill.title }), _jsxs("p", { children: [saved === null && !error && "Reading your desktop…", saved !== null && onDesktop && (_jsxs("span", { role: "status", children: ["On your desktop \u00B7", " ", saved.map((value) => sizeLabels[value]).join(", ")] })), saved !== null &&
|
|
83
|
+
!onDesktop &&
|
|
84
|
+
"Choose a size, then add this widget to your desktop.", error ? _jsx("span", { role: "alert", children: error }) : null] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", style: { gridRow: 2, gridColumn: "1 / -1" }, children: [!onDesktop ? (_jsx("div", { style: { width: "7rem", flex: "0 0 auto" }, children: _jsx(Select, { "aria-label": `Size for ${gill.title}`, value: size, disabled: pending || saved === null || !consentReady, onChange: (event) => {
|
|
85
|
+
const next = sizes.find((value) => value === event.currentTarget.value);
|
|
86
|
+
if (next)
|
|
87
|
+
setSize(next);
|
|
88
|
+
}, children: sizes.map((value) => (_jsx("option", { value: value, children: sizeLabels[value] }, value))) }) })) : null, _jsx(Button, { size: "sm", variant: "secondary", loading: pending, disabled: pending || saved === null || !consentReady, onClick: (event) => {
|
|
89
|
+
if (event.detail > 1)
|
|
90
|
+
return;
|
|
91
|
+
void change();
|
|
92
|
+
}, children: onDesktop ? "Remove from desktop" : "Add to desktop" })] })] }));
|
|
93
|
+
}
|
|
94
|
+
/** Suggestions only: the member chooses, then the admitted dashboard arranges. */
|
|
95
|
+
export function AppDesktopSettings({ app, desktop, consentReadyAt = 0, }) {
|
|
96
|
+
const gills = app.availability === "available"
|
|
97
|
+
? app.gills.filter((gill) => gill.desktop)
|
|
98
|
+
: [];
|
|
99
|
+
return (_jsxs("section", { "aria-label": "Desktop widgets", children: [_jsx(DataListHeading, { level: 3, children: "Desktop widgets" }), _jsx("p", { children: "Add this App\u2019s widgets to your personal desktop. Your Team default stays unchanged." }), gills.length ? (_jsx(DataList, { "aria-label": "Available desktop widgets", children: gills.map((gill) => (_jsx(GillOption, { app: app, gill: gill, desktop: desktop, consentReadyAt: consentReadyAt }, gill.id))) })) : (_jsx("p", { children: app.availability === "available"
|
|
100
|
+
? "This App offers no desktop widgets."
|
|
101
|
+
: "This App is unavailable. Its widgets cannot be added." }))] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type AppStatusAction } from "@fungi.computer/agar";
|
|
2
|
+
import { createAppWindowManager, type AppWindowProjection } from "./app-windows.js";
|
|
3
|
+
import { type AppDirectoryApp } from "./app-launch.js";
|
|
4
|
+
/** The focused window's App-level settings; its Computer lives in its status bar. */
|
|
5
|
+
export declare function FocusedWindowControls({ app, onOpenSettings, onOpenPermissions, projection, }: {
|
|
6
|
+
app: AppDirectoryApp | undefined;
|
|
7
|
+
onOpenSettings: () => void;
|
|
8
|
+
onOpenPermissions: () => void;
|
|
9
|
+
projection: AppWindowProjection;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
11
|
+
export declare function AppPane({ projection, icon, manager, onError, index, focusWindow, afterHide, onBack, onSelectComputer, }: {
|
|
12
|
+
projection: AppWindowProjection;
|
|
13
|
+
icon: string | null | undefined;
|
|
14
|
+
manager: ReturnType<typeof createAppWindowManager>;
|
|
15
|
+
onError: (message: string) => void;
|
|
16
|
+
index: number;
|
|
17
|
+
focusWindow: (id: string) => void;
|
|
18
|
+
afterHide: (id: string) => void;
|
|
19
|
+
onBack: (id: string) => void;
|
|
20
|
+
/** The person chose another of this window's offered Computers. */
|
|
21
|
+
onSelectComputer: (windowId: string, computerId: string) => void;
|
|
22
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
/**
|
|
24
|
+
* One window's status bar, owned by the shell like its title bar: the
|
|
25
|
+
* window's Computer first (only the person changes it, here), then the items
|
|
26
|
+
* the App declared as data. The shell renders every item with Caps pieces and
|
|
27
|
+
* hands the person's use back to the App. A window with neither has no bar.
|
|
28
|
+
*/
|
|
29
|
+
export declare function WindowStatusBar({ projection, onSelectComputer, onAction, surface, computerPickerOpen, onComputerPickerOpenChange, }: {
|
|
30
|
+
projection: AppWindowProjection;
|
|
31
|
+
onSelectComputer: (computerId: string) => void;
|
|
32
|
+
onAction: (action: AppStatusAction) => void;
|
|
33
|
+
/** The App's own surface, which UI opened from the bar is placed against. */
|
|
34
|
+
surface?: () => HTMLElement | null;
|
|
35
|
+
computerPickerOpen?: boolean;
|
|
36
|
+
onComputerPickerOpenChange?: (open: boolean) => void;
|
|
37
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|