From c14ed9b164bc5868224ef9ece614fe54f470441e Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 23 Oct 2026 07:11:18 +0200 Subject: [PATCH 01/10] docs(adr): propose SSH Computers started like Termux Add ADR-0069: an App Shell installs, starts, and keeps the App Server on a Linux computer over SSH, holds its token, and forwards its loopback endpoint through the SSH connection. Covers version cases with confirmed updates, a keeper client for lifetime, system ssh on Desktop and an embedded client on Android. Add the SSH Computer term to CONTEXT.md. --- CONTEXT.md | 4 ++ ...9-ssh-computers-are-started-like-termux.md | 60 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 docs/adr/0069-ssh-computers-are-started-like-termux.md diff --git a/CONTEXT.md b/CONTEXT.md index 31b6848d..eb869421 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -299,6 +299,10 @@ _Avoid_: Account, login, linked phone A short plain-text code, also drawn as a QR code, that adds a Remote Device. Either a trusted client shows a single-use code the new device reads, or the new device shows a code a trusted client reads and confirms. _Avoid_: Password, token, invite link +**SSH Computer**: +A Linux computer reached over SSH where an App Shell installs, starts, and keeps the App Server, and holds its token like a same-machine App Shell. It is not a Remote Device: no Pairing Code is involved. +_Avoid_: Remote host, server, SSH remote + **Desktop Update**: A newer official OpenAIDE Desktop App release offered inside the installed Desktop App. The user may review its Release Notes, initiate download and staging, and restart OpenAIDE to apply it. _Avoid_: Silently replacing a running release, treating a downloaded update as already applied diff --git a/docs/adr/0069-ssh-computers-are-started-like-termux.md b/docs/adr/0069-ssh-computers-are-started-like-termux.md new file mode 100644 index 00000000..a075617c --- /dev/null +++ b/docs/adr/0069-ssh-computers-are-started-like-termux.md @@ -0,0 +1,60 @@ +# SSH Computers Are Started Like Termux + +Status: proposed + +The Desktop App and the Mobile App can make a Linux computer reachable over SSH the place where agents work. The App Shell installs the App Server there, starts it, and reaches it the way the Mobile App reaches its App Server in Termux: it holds the per-process token and is a client of that App Server, with the loopback endpoint carried through the SSH connection instead of a local socket. No Pairing Code, key exchange with the App Server, or relay is involved; the user's SSH access is the proof. Pairing with a computer ([ADR-0063](0063-desktop-can-use-another-computers-app-server.md)) stays the way to reach an App Server someone else started. + +## Choice + +An SSH Computer is one more choice next to this computer and paired computers. Exactly one is in use at a time, and changing it restarts the App Shell as ADR-0063 does. The first release supports Linux on x64 only. + +The App Server on an SSH Computer uses that computer's default state root, the one the `remote` command uses ([ADR-0064](0064-a-terminal-command-keeps-the-app-server-reachable.md)). A computer has one OpenAIDE: Projects and Tasks started over SSH are the ones its paired devices see. + +### Versions + +The App Shell installs its own App Server release next to earlier ones, under one directory per version in the user's home. On connecting it finds one of four cases: + +- **Same version running, or none**: it attaches, or starts one. +- **Older version, compatible protocol**: it attaches like a Remote Device, by protocol compatibility ([ADR-0062](0062-remote-devices-are-app-server-clients.md)), and offers an update. +- **Older version, incompatible protocol**: it cannot attach until the computer is updated. +- **Newer version**: it refuses and asks the user to update this App Shell. + +An update happens only after the user confirms. The confirmation lists the other clients attached to that App Server with their kind, name, and version, marks the ones the update will strand, and counts running and waiting Tasks. Confirming sends the existing replacement request. An App Shell never replaces a newer App Server with an older one, so two clients of different versions cannot replace each other in turn. + +### Lifetime + +The App Server's lifetime rule does not change: it stops after its last same-machine client leaves. The App Shell starts a windowless keeper on the SSH Computer, a client like the `remote` command, outside the SSH session. The keeper stays while any of these holds: + +- a Task is running; +- a Task has waited for the user for less than 24 hours; +- an App Shell was attached within the last 30 minutes. + +When none holds, the keeper leaves and the App Server stops by the existing rule. A Task that was waiting is then interrupted and is resumed later the way any Task with a Native Session is. The App Shell also offers an explicit stop, which warns about running Tasks. + +## Mechanism + +- One Rust implementation in `openaide-remote-client` does the launch: detect the system and architecture, upload and verify the release, start the App Server through the user's login shell, read the endpoint line, apply the version cases, start the keeper, and forward the endpoint. It needs only two things from SSH: running a command and opening a channel to a loopback port. +- The Desktop App provides those through the system `ssh`, so the user's SSH configuration, jump hosts, agent, hardware keys, and interactive prompts keep working. Prompts are shown in the App Shell and never stored. +- The Mobile App embeds an SSH client. The phone generates its own key, kept encrypted with Android Keystore like its pairing key, and shows the public key to add on the computer. A password may be used once to add that key, and is not stored. Jump hosts are not supported on the phone at first. +- The App Shell downloads the matching release asset once, checks it against the SHA-256 built into the App Shell, and uploads it over SSH. The SSH Computer needs no internet access to receive it. The Windows Desktop reuses the Linux App Server it already bundles for WSL. +- Agents are installed and signed in on the SSH Computer, as in Termux. When Node is missing, a pinned Node runtime is installed in the user's home without elevated rights. +- The launcher detects a login manager that ends a user's processes when the SSH session closes and names the setting that keeps the keeper alive. +- The App Server Protocol gains the list of attached clients with kind, name, product version, and protocol version, as a minor version change. +- On the Mobile App, Task notifications for an SSH Computer come from the same foreground-service watch used for a paired computer, held over the SSH connection. +- Each launch stage, from connecting to forwarding, emits structured start and end events with a correlation identifier, outcome, duration, attempt count, and classified error, and no host names, user names, paths, or command output. + +## Considered Options + +- **SSH only to install, then pairing over iroh.** Rejected as the main path: traffic would depend on public relays, which many networks block, while SSH already reaches the computer. Pairing from an attached App Shell remains available. +- **A separate state root for SSH.** Rejected: it would split one computer's Projects and Native Sessions in two. +- **An idle timer in the App Server.** Rejected for the reason ADR-0064 gives: a second lifetime rule. The keeper is a client and decides only when to leave. +- **Updating without confirmation.** Rejected: replacement ends running Tasks and can strand other clients. +- **One embedded SSH client everywhere.** Rejected for the Desktop, because it would break existing SSH configurations. +- **The SSH Computer downloading the release itself.** Rejected: it needs internet access on the computer and a second download path. + +## Consequences + +- Secrets a user enters for an Agent or MCP server stay in the App Shell that entered them (ADR-0063), so Tasks that run while no App Shell is attached cannot read them. The App Shell says so. Moving that storage to the App Server's computer is a separate decision. +- Linux on arm64, macOS, and Windows computers need their own App Server builds and launch paths. +- The macOS Desktop and the Mobile App need network access once per release to fetch the Linux App Server. +- A waiting Task can keep the App Server alive for up to a day after its user left. From 8eac50f5e7914ff6bc0df5a804151890f1103f65 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 23 Oct 2026 07:19:18 +0200 Subject: [PATCH 02/10] docs(adr): switch computers in place and keep work running ADR 0070 records the computer switcher: a compact control at the end of the Settings row once a computer is paired, one row per App Server, connect before switching, a prompt before discarding unsent input, and returning to the last place on each computer. Leaving a computer never stops its work, both App Shells notify for this device's Tasks, and the Desktop switches by reloading its WebView instead of restarting. ADR 0063 points its restart and no-App-Server choices at ADR 0070. --- ...op-can-use-another-computers-app-server.md | 8 ++-- ...ters-is-in-place-and-keeps-work-running.md | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 docs/adr/0070-switching-computers-is-in-place-and-keeps-work-running.md diff --git a/docs/adr/0063-desktop-can-use-another-computers-app-server.md b/docs/adr/0063-desktop-can-use-another-computers-app-server.md index 359a60a3..c22f1114 100644 --- a/docs/adr/0063-desktop-can-use-another-computers-app-server.md +++ b/docs/adr/0063-desktop-can-use-another-computers-app-server.md @@ -1,16 +1,16 @@ # Desktop Can Use Another Computer's App Server -Status: accepted +Status: accepted; restart and App Server choices superseded by ADR-0070 The Desktop App on one computer can work with the App Server of another computer, the same way the Mobile App does ([ADR-0062](0062-remote-devices-are-app-server-clients.md)). It pairs by Pairing Code, is trusted by its own key pair, and reaches the other App Server over iroh. No account, address, or password is involved. ## Choice -A Desktop keeps its own App Server. The user chooses one place where agents work: this computer, or one of the computers it is paired with. Exactly one is in use at a time, and the choice is a Desktop bootstrap preference next to the runtime environment, so changing it restarts the Desktop like an environment change does. Pairing with a computer selects it. +A Desktop keeps its own App Server. The user chooses one place where agents work: this computer, or one of the computers it is paired with. Exactly one is in use at a time, and the choice is a Desktop bootstrap preference next to the runtime environment. Pairing with a computer selects it. Changing the choice no longer restarts the Desktop, and leaving this computer no longer stops its App Server ([ADR-0070](0070-switching-computers-is-in-place-and-keeps-work-running.md)). A Desktop pairs only in the invite direction: the other computer shows a code and this Desktop spends it. Between two computers either direction costs the user the same copy and paste, so the join-request direction and its listening endpoint are left out. -When a paired computer is in use the Desktop starts no App Server. Startup first reaches the paired computer, so an unreachable computer is named on the startup screen together with the way back to this computer. A removal or an incompatible version found later is reported once, with the same way back. +Startup with a paired computer in use first reaches the paired computer, so an unreachable computer is named on the startup screen together with the way back to this computer. A removal or an incompatible version found later is reported once, with the same way back. ## Mechanism @@ -29,7 +29,7 @@ While a paired computer is in use, the parts of the Desktop that assume a same-m - **A Desktop that is only a remote client.** Rejected: a computer that can run agents should keep doing so, and switching back must not need a reinstall. - **Using several computers at once.** Rejected for now: the Frontend renders one App Server's state, and merging Projects and Tasks of several is a product change of its own. -- **Applying the choice without a restart.** Rejected for now: the Frontend's session, caches, and shell capabilities are created once for one App Server. The restart is the mechanism the runtime environment already uses. +- **Applying the choice without a restart.** Rejected at first because the Frontend's session, caches, and shell capabilities are created once for one App Server; later adopted by reloading the WebView in a kept process ([ADR-0070](0070-switching-computers-is-in-place-and-keeps-work-running.md)). - **A second copy of the client in TypeScript inside the WebView.** Rejected: iroh has no WebView binding here, and the key must not live in web storage. ## Consequences diff --git a/docs/adr/0070-switching-computers-is-in-place-and-keeps-work-running.md b/docs/adr/0070-switching-computers-is-in-place-and-keeps-work-running.md new file mode 100644 index 00000000..39ca907b --- /dev/null +++ b/docs/adr/0070-switching-computers-is-in-place-and-keeps-work-running.md @@ -0,0 +1,40 @@ +# Switching Computers Is In Place and Keeps Work Running + +Status: accepted + +The Desktop App and the Mobile App let the user switch the computer in use from the Task navigation, without visiting Settings and without restarting. Switching changes which App Server the App Shell renders; it never stops work on the computer left behind. This replaces two choices of [ADR-0063](0063-desktop-can-use-another-computers-app-server.md): that a change restarts the Desktop, and that a Desktop using another computer starts no App Server. It applies to every kind of computer: this one, a paired computer, and an SSH Computer ([ADR-0069](0069-ssh-computers-are-started-like-termux.md)). + +## Choice + +Exactly one computer is in use at a time, as before. Merging several App Servers' Projects and Tasks into one navigation remains rejected. + +- **Switcher.** Once at least one computer is paired or added over SSH, a compact control sits at the end of the Settings row of the Task navigation. It names the computer in use with its connection state, and shows only an icon while this device is in use. It opens in place a list of this device first, then the other computers in the order they were added, with the one in use checked and the last item leading to Settings, Remote. Other computers' reachability is not probed. A user who never pairs sees no new control. The Desktop opens it with Ctrl/Cmd+Shift+S, and digits 1 to 9 pick a row. The shared Frontend owns it; each App Shell only applies the switch. +- **One row per App Server.** A row stands for one App Server, identified by its key, not for a route to it. When an SSH Computer turns out to host an App Server that is also paired, the two become one row with two routes; a switch tries the route that worked last and falls back to the other. +- **Connect first.** The switch connects to the new computer while the current one stays in use, shows Connecting with Cancel, and moves only once connected. An SSH prompt during that connection is answered there; cancelling it cancels the switch. A failure is shown in the list and leaves the user where they were. +- **Unsent input.** When the page holds an unsent draft, a selected attachment, or an upload in progress, the switch asks before discarding it. Otherwise it asks nothing. +- **Place.** Each App Shell remembers, per computer, the last Task, Project, or New Task Project the user had open, and returns there; a place that no longer exists, or a first visit, opens that computer's start page. This is presentation state kept on the device. +- **Work keeps running.** The Desktop keeps this computer's App Server held while another computer is in use, and releases it only when the Desktop quits. The Mobile App keeps the protection it gives the phone's own running Tasks while another computer is in use. +- **Attention.** Both App Shells notify for this device's Tasks, as well as the computer in use's. Activating such a notification switches back and opens the Task. Other computers are not watched. + +## Mechanism + +The Desktop keeps its process and replaces the connection: it ends the gateway and connection to the computer it leaves, starts or rejoins the target, and reloads the WebView so the Frontend, its caches, and shell capabilities are created again for one App Server, as the Mobile App already does. Each switch uses a new client instance identifier, so a server still holding the previous one in its reconnect grace does not hand back stale state. Connection failures are reported per computer rather than once per run. + +The Mobile App sends the list of computers and the one in use to every page, and accepts reading that state and the two switch commands from any page; every other connection command stays limited to the Settings sections. + +A switch emits structured start and terminal events under one correlation identifier from the choice through the reloaded Frontend's first ready state, with source and target kinds, route, outcome, duration, and classified error, and no names, keys, or addresses. + +## Considered Options + +- **Keep the restart.** Rejected: it made switching slow and lost the user's place, and the cost was paid on every switch. +- **Swap the connection inside the live page.** Rejected: the Frontend's session, caches, and shell capabilities are created once per load; reloading the WebView rebuilds them with the code that already exists. +- **Switch first, then report an unreachable computer.** Rejected: it leaves the user on a dead computer and could discard a draft for nothing. +- **Stop this computer's App Server when another is in use.** Rejected: switching would silently interrupt running Tasks and cut off this computer's own Remote Devices. +- **A switcher row or header shown to every user.** Rejected: most users never pair, and the control would make remote work look like the main path. +- **One row per route.** Rejected: two rows for the same Projects and Tasks invite the user to wonder which is real. + +## Consequences + +- A Desktop using another computer still runs its own App Server and the agents in it, so it uses this computer's resources until it quits. +- Unsent drafts are still lost on a confirmed switch. Keeping them per computer and Task is a separate decision. +- Two different App Servers on one machine, such as the Desktop's own state root and the default root an SSH Computer uses, appear as two rows told apart by their kind. Whether they should be one App Server belongs to ADR-0069 and the shared state root question of [ADR-0064](0064-a-terminal-command-keeps-the-app-server-reachable.md). From ef191b78405bcfcf8dc89544f779cac5fc0aefd8 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 23 Oct 2026 07:24:00 +0200 Subject: [PATCH 03/10] docs(plans): plan the computer switcher in four slices Records the findings behind ADR 0070 and the slices: the shared switcher with one computers capability, the in-place Desktop switch that keeps this computer's App Server, connect-first with place restore, and notifications for this device's Tasks. --- docs/plans/computer-switcher.md | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/plans/computer-switcher.md diff --git a/docs/plans/computer-switcher.md b/docs/plans/computer-switcher.md new file mode 100644 index 00000000..ae607aba --- /dev/null +++ b/docs/plans/computer-switcher.md @@ -0,0 +1,53 @@ +# Computer switcher + +Status: agreed design ([ADR-0070](../adr/0070-switching-computers-is-in-place-and-keeps-work-running.md)); implementation planned in four independently shippable slices. + +## Pre-implementation findings + +- Both App Shells already keep several computers. Switching lives only in Settings, Remote (`RemoteSection.tsx`), behind two different shell capabilities: `connectionSettings` on the Mobile App and `remoteComputers` on the Desktop. +- `SidebarComputer` renders in the Desktop title bar and in the sidebar actions of other shells, and only while another computer is in use. On the Mobile App it never appears on a fresh page: the native side sends connection state only while a Settings section is open (`WorkspaceConnectionBridge.java:34`, `SetupPagePolicy.java:18`). +- The Desktop applies a switch with a full process relaunch (`restart_desktop`, `main.rs`). Obstacles to switching in place: `desktop_bootstrap` returns a cached connection, the remote gateway and client cannot be stopped (`remote-client/src/gateway.rs`), the failure dialog fires once per run, and `clientInstanceId` is per process. +- Leaving this computer on the Desktop lets its App Server drain about 40 seconds later (heartbeat timeout plus reconnect grace), interrupting running Tasks and stopping its Remote Devices endpoint. The Mobile App leaves Termux running but drops the wake lock protecting local work. +- Drafts live only in Frontend memory, and attachment handles belong to a client instance; any switch loses both. +- No App Shell remembers the last place: the Desktop route is an in-memory closure, the Mobile App loads `/` after each switch. +- No command palette exists; macOS has a native menu, other platforms map Ctrl shortcuts in the WebView (`desktopCommands.ts`). +- `main.rs` is above the 800-line limit and `AppSurfaces.tsx` is close to it; both need an extraction before they grow. +- Switch diagnostics have no source/target, outcome, end-to-end duration, or correlation across the restart. + +## Slice 1: shared switcher and Mobile App state on every page + +- Replace `connectionSettings`' computer fields and `remoteComputers` with one `computers` shell capability: snapshot (this device, computers with id, name, kind, current, link), subscribe, `use(id)`, `cancel()`. Each shell adapts its native side to it. The Settings, Remote page reads the same capability. +- `ComputerSwitcher` in the sidebar footer's Settings row, shown once any computer exists: name and dot on another computer, icon on this device, words only for an unhealthy link. An upward listbox: this device, then computers in the order added, current checked, last item "Pair or manage computers…". Remove `SidebarComputer` and the Desktop title-bar lead. +- Ctrl/Cmd+Shift+S opens it (`desktopCommands` gains `switch-computer`, and macOS File ▸ Switch Computer…); arrows, Enter, and 1 to 9 inside. +- Mobile App: push computer state to every page; accept `state`, `use_computer`, and `local` from any route; all other commands stay Settings-only. +- Tests: switcher unit and DOM tests (unpaired shows nothing, local icon, remote name, keyboard, narrow truncation); Android bridge policy test for the widened commands; one wide and one narrow viewport check. + +## Slice 2: Desktop switches in place and keeps its App Server + +- Extract the connection ownership from `main.rs` into a module that owns the current target and can replace it: local handoff, or remote gateway. Give `Gateway` and `RemoteClient` a stop handle in `openaide-remote-client`. +- New async command applies a switch: connect the target, retire the previous connection, issue a new client instance id, return the new bootstrap; the WebView reloads into it. Failure reporting is per computer, not once per run. +- The App Server hold always targets this computer's App Server and ends only on quit or update; startup with another computer in use also starts it, so switching back, this device's notifications, and this computer's own Remote Devices never depend on an earlier visit. Sleep recovery keeps working for it while another computer is in use. +- Runtime environment changes keep their restart. +- Tests: remote-client gateway stop and reconnect against the real App Server edge; Rust tests for the switch state machine (success, failure keeps current, cancel); a Desktop shell test that the WebView reload receives the new connection. + +## Slice 3: connect first, unsent input, and place + +- Connect first on both App Shells: the target row shows Connecting with Cancel; failure is shown in the list; nothing changes until connected. Android `RemoteNode` connects the target before disconnecting the previous one. +- Before applying a connected switch, the Frontend asks when the page has a draft, a selected attachment, or an upload in progress; declining closes the new connection. +- Place: the Frontend records the last Task, Project, or New Task Project per computer id in device storage and restores it after the first replica load, falling back to the start page when it no longer exists. Settings is never restored. Add an inline TODO where drafts are dropped, naming per-computer draft persistence as the follow-up. +- Tests: switcher state for connect, fail, cancel, and confirm; place restore with an existing and a missing Task. + +## Slice 4: notifications for this device + +- Depends on `taskAttention/wait` from `shushakov/paired-task-notifications`; rebase after it merges. +- Mobile App: the background service watches the phone's App Server as well as the selected computer, and keeps the wake lock while local Tasks run. A tap on a local notification switches to this phone and opens the Task. +- Desktop: while another computer is in use, the Desktop shell keeps a held `taskAttention/wait` on this computer's App Server and notifies through the existing Desktop notification manager; activation switches back and opens the Task. +- Tests: the watch's baseline, in-sight, and one-per-Task rules for the second source; activation routing. + +## Diagnostics + +Each switch emits `computer_switch` start and terminal events under one correlation id carried across the reload: source and target kind, route, attempt, outcome (`switched`, `failed`, `cancelled`, `declined`), duration to the reloaded Frontend's first ready state, and classified error. No names, keys, addresses, or paths. The Desktop log and Android diagnostics both record it, and Support Export includes the Desktop's. + +## Open questions + +- SSH Computers join the list once ADR-0069 lands; the capability's `kind` and route fields are shaped for them now. From cba187a423aa0805ac1020505c02d5bbcf106d41 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 23 Oct 2026 07:39:08 +0200 Subject: [PATCH 04/10] feat(frontend): switch computers from the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a compact computer switcher at the end of the sidebar's Settings row (ADR-0070). It appears only once another computer is paired, names the computer in use with a dot for a healthy link, and lists this device first, then computers in the order added. Ctrl/Cmd+Shift+S and macOS File > Switch Computer… open it; 1 to 9 pick a row. Both App Shells now expose one `computers` capability, which Settings, Remote also reads. The Mobile App sends its computer list to every page and accepts switching from any route; every other connection command stays on Settings. The Desktop title-bar computer header is removed. The Desktop still applies a switch by restarting until slice 2. --- .../android/ConnectionController.java | 3 + .../io/openaide/android/SetupPagePolicy.java | 9 + .../android/WorkspaceConnectionBridge.java | 16 +- .../openaide/android/SetupPagePolicyTest.java | 7 + apps/desktop/frontend/desktopCommands.test.ts | 5 + apps/desktop/frontend/desktopCommands.ts | 5 +- .../desktop/frontend/desktopComputers.test.ts | 52 + apps/desktop/frontend/desktopComputers.ts | 44 + apps/desktop/frontend/desktopShell.ts | 11 +- .../src-tauri/gen/schemas/linux-schema.json | 2879 +++++++++++++++++ apps/desktop/src-tauri/src/macos_menu.rs | 51 + apps/desktop/src-tauri/src/main.rs | 52 +- .../androidConnectionSettings.test.ts | 26 + .../web/frontend/androidConnectionSettings.ts | 26 +- apps/web/frontend/webAppShell.ts | 1 + docs/plans/computer-switcher.md | 6 +- .../frontend/src/components/AppSurfaces.tsx | 32 +- .../components/ComputerSwitcher.dom.test.tsx | 164 + .../src/components/ComputerSwitcher.tsx | 164 + .../src/components/DesktopTitleBar.tsx | 7 +- packages/frontend/src/components/Sidebar.tsx | 59 +- .../src/components/SidebarComputer.test.tsx | 83 - .../src/components/SidebarComputer.tsx | 80 - .../components/settings/GeneralSection.tsx | 3 +- .../src/components/settings/RemoteSection.tsx | 237 +- .../settings/SettingsSections.dom.test.tsx | 22 +- .../src/components/settings/settingsKit.tsx | 4 +- packages/frontend/src/services/computers.ts | 53 + .../src/services/connectionSettings.ts | 4 - .../frontend/src/services/frontendShell.ts | 25 +- packages/frontend/src/styles/app/part-00.css | 137 +- 31 files changed, 3784 insertions(+), 483 deletions(-) create mode 100644 apps/desktop/frontend/desktopComputers.test.ts create mode 100644 apps/desktop/frontend/desktopComputers.ts create mode 100644 apps/desktop/src-tauri/gen/schemas/linux-schema.json create mode 100644 apps/desktop/src-tauri/src/macos_menu.rs create mode 100644 packages/frontend/src/components/ComputerSwitcher.dom.test.tsx create mode 100644 packages/frontend/src/components/ComputerSwitcher.tsx delete mode 100644 packages/frontend/src/components/SidebarComputer.test.tsx delete mode 100644 packages/frontend/src/components/SidebarComputer.tsx create mode 100644 packages/frontend/src/services/computers.ts diff --git a/apps/android/app/src/main/java/io/openaide/android/ConnectionController.java b/apps/android/app/src/main/java/io/openaide/android/ConnectionController.java index 18bb2787..4e9194d2 100644 --- a/apps/android/app/src/main/java/io/openaide/android/ConnectionController.java +++ b/apps/android/app/src/main/java/io/openaide/android/ConnectionController.java @@ -282,6 +282,9 @@ void onActivityResult(int request, int result, Intent data) { if (code != null) pair(code); } + /** Sends the current state without rechecking this phone's setup. */ + void refresh() { emit(); } + void resume() { if (recheckOnReturn && !busy) { recheckOnReturn = false; check(); } else emit(); diff --git a/apps/android/app/src/main/java/io/openaide/android/SetupPagePolicy.java b/apps/android/app/src/main/java/io/openaide/android/SetupPagePolicy.java index 040cc027..ad6a89e4 100644 --- a/apps/android/app/src/main/java/io/openaide/android/SetupPagePolicy.java +++ b/apps/android/app/src/main/java/io/openaide/android/SetupPagePolicy.java @@ -17,6 +17,15 @@ static boolean opensSettings(ConnectionProfile profile, String source, String de /** Settings sections that show phone controls: background work, paired computers, phone setup. */ private static final java.util.Set PHONE_SECTIONS = java.util.Set.of("general", "remote", "troubleshooting"); + /** + * Switching the computer in use works from every page, so the switcher in the Task + * navigation needs no Settings route (ADR-0070). Every other connection command + * changes this phone's setup and stays limited to the Settings sections. + */ + private static final java.util.Set EVERY_PAGE_ACTIONS = java.util.Set.of("use_computer", "local"); + + static boolean allowedOnEveryPage(String action) { return EVERY_PAGE_ACTIONS.contains(action); } + static boolean isConnectionRoute(ConnectionProfile profile, String address) { if (!profile.owns(address)) return false; try { diff --git a/apps/android/app/src/main/java/io/openaide/android/WorkspaceConnectionBridge.java b/apps/android/app/src/main/java/io/openaide/android/WorkspaceConnectionBridge.java index 3f22332f..16c6a612 100644 --- a/apps/android/app/src/main/java/io/openaide/android/WorkspaceConnectionBridge.java +++ b/apps/android/app/src/main/java/io/openaide/android/WorkspaceConnectionBridge.java @@ -31,7 +31,11 @@ final class WorkspaceConnectionBridge { this.visible = visible; appearance = new SystemBarAppearance(activity, browser); controller = new ConnectionController(activity, new ConnectionController.View() { - @Override public void render(JSONObject state) { if (settingsRoute()) send("state", "state", state); } + @Override public void render(JSONObject state) { + // Every page gets the computers for the switcher; only Settings gets the setup state. + send("computers", "computers", state.optJSONArray("computers")); + if (settingsRoute()) send("state", "state", state); + } @Override public void changed() { changed.run(); } @Override public void close() { } @Override public void setup(String screen) { setup.accept(screen); } @@ -53,14 +57,14 @@ void attach() { return; } int id = request.getInt("id"); + JSONObject command = request.getJSONObject("command"); long started = android.os.SystemClock.elapsedRealtime(); diagnostics.record("connection_controls", "started", id, 0); - if (!visible.getAsBoolean() || !settingsRoute()) { + if (!visible.getAsBoolean() || (!settingsRoute() && !SetupPagePolicy.allowedOnEveryPage(command.optString("action")))) { diagnostics.record("connection_controls", "denied", id, android.os.SystemClock.elapsedRealtime() - started); reply(id, false); return; } - JSONObject command = request.getJSONObject("command"); if (command.optString("action").startsWith("update_")) updates.command(activity, command); else controller.receive(command); diagnostics.record("connection_controls", "accepted", id, android.os.SystemClock.elapsedRealtime() - started); @@ -71,6 +75,7 @@ void attach() { browser.postWebMessage(new WebMessage("openaide:connection-controls:1", new WebMessagePort[]{channel[1]}), Uri.parse(profile.endpoint.substring(0, profile.endpoint.length() - 1))); if (settingsRoute()) controller.resume(); + else controller.refresh(); // Every workspace page gets the update state, so navigation can mark an available update. updatesSubscription = updates.subscribe(state -> send("update", "update", state)); } @@ -89,7 +94,10 @@ private void reply(int id, boolean accepted) { catch (Exception ignored) { } } - void resume() { if (settingsRoute()) controller.resume(); } + void resume() { + if (settingsRoute()) controller.resume(); + else controller.refresh(); + } void onRequestPermissionsResult(int request, String[] permissions, int[] results) { controller.onRequestPermissionsResult(request, permissions, results); } void onActivityResult(int request, int result, Intent data) { controller.onActivityResult(request, result, data); } void detach() { diff --git a/apps/android/app/src/test/java/io/openaide/android/SetupPagePolicyTest.java b/apps/android/app/src/test/java/io/openaide/android/SetupPagePolicyTest.java index ee18b6f3..e7d8f695 100644 --- a/apps/android/app/src/test/java/io/openaide/android/SetupPagePolicyTest.java +++ b/apps/android/app/src/test/java/io/openaide/android/SetupPagePolicyTest.java @@ -15,6 +15,13 @@ public class SetupPagePolicyTest { assertFalse(SetupPagePolicy.isConnectionRoute(profile, address)); } + @Test public void onlySwitchingComputersWorksFromEveryPage() { + assertTrue(SetupPagePolicy.allowedOnEveryPage("use_computer")); + assertTrue(SetupPagePolicy.allowedOnEveryPage("local")); + for (String action : new String[]{"state", "forget_computer", "pair_setup", "install", "grant", "background", "diagnostics", ""}) + assertFalse(SetupPagePolicy.allowedOnEveryPage(action)); + } + @Test public void servesOnlyBundledSetupAssets() { assertEquals("setup/index.html", SetupPagePolicy.asset("https://app.openaide.invalid/index.html")); assertEquals("setup/app.js", SetupPagePolicy.asset("https://app.openaide.invalid/app.js")); diff --git a/apps/desktop/frontend/desktopCommands.test.ts b/apps/desktop/frontend/desktopCommands.test.ts index 928c5521..9ac789de 100644 --- a/apps/desktop/frontend/desktopCommands.test.ts +++ b/apps/desktop/frontend/desktopCommands.test.ts @@ -10,6 +10,11 @@ describe("Desktop keyboard commands", () => { expect(desktopCommandForKeyboardEvent({ altKey: false, ctrlKey: true, key, metaKey: false, shiftKey: false })).toBe(command); }); + it("opens the computer switcher with Ctrl+Shift+S", () => { + expect(desktopCommandForKeyboardEvent({ altKey: false, ctrlKey: true, key: "S", metaKey: false, shiftKey: true })).toBe("switch-computer"); + expect(desktopCommandForKeyboardEvent({ altKey: false, ctrlKey: true, key: "s", metaKey: false, shiftKey: false })).toBeUndefined(); + }); + it("leaves modified and unrelated keys to the focused control", () => { expect(desktopCommandForKeyboardEvent({ altKey: false, ctrlKey: true, key: "n", metaKey: false, shiftKey: true })).toBeUndefined(); expect(desktopCommandForKeyboardEvent({ altKey: false, ctrlKey: false, key: "n", metaKey: false, shiftKey: false })).toBeUndefined(); diff --git a/apps/desktop/frontend/desktopCommands.ts b/apps/desktop/frontend/desktopCommands.ts index 3b133ed6..4e859fbd 100644 --- a/apps/desktop/frontend/desktopCommands.ts +++ b/apps/desktop/frontend/desktopCommands.ts @@ -1,4 +1,4 @@ -export type DesktopCommand = "check-for-updates" | "new-task" | "open-project" | "quit" | "settings"; +export type DesktopCommand = "check-for-updates" | "new-task" | "open-project" | "quit" | "settings" | "switch-computer"; export type DesktopSurfaceCommand = Exclude; type KeyboardCommandEvent = Pick< @@ -7,7 +7,8 @@ type KeyboardCommandEvent = Pick< >; export function desktopCommandForKeyboardEvent(event: KeyboardCommandEvent): DesktopSurfaceCommand | undefined { - if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return undefined; + if (!event.ctrlKey || event.metaKey || event.altKey) return undefined; + if (event.shiftKey) return event.key.toLowerCase() === "s" ? "switch-computer" : undefined; if (event.key.toLowerCase() === "n") return "new-task"; if (event.key.toLowerCase() === "o") return "open-project"; if (event.key === ",") return "settings"; diff --git a/apps/desktop/frontend/desktopComputers.test.ts b/apps/desktop/frontend/desktopComputers.test.ts new file mode 100644 index 00000000..66e83ebb --- /dev/null +++ b/apps/desktop/frontend/desktopComputers.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createDesktopComputers } from "./desktopComputers"; + +const studio = { id: "a".repeat(64), name: "Studio", current: false }; + +afterEach(() => vi.restoreAllMocks()); + +function invoker(answers: Record Promise>) { + return vi.fn((command: string, _args?: Record) => answers[command]?.() ?? Promise.resolve(undefined)) as never; +} + +describe("Desktop computers", () => { + it("publishes the paired computers once the shell has read them", async () => { + const invoke = invoker({ desktop_remote_snapshot: async () => [studio] }); + const computers = createDesktopComputers(invoke); + const changed = vi.fn(); + computers.subscribe(changed); + expect(computers.snapshot()).toBeUndefined(); + await vi.waitFor(() => expect(changed).toHaveBeenCalled()); + expect(computers.snapshot()).toEqual({ device: "computer", computers: [studio] }); + expect(computers.snapshot()).toBe(computers.snapshot()); + }); + + it("says when the list could not be read and reads it again for the next viewer", async () => { + vi.spyOn(console, "warn").mockImplementation(() => undefined); + let fail = true; + const invoke = invoker({ desktop_remote_snapshot: async () => { if (fail) throw new Error("storage"); return [studio]; } }); + const computers = createDesktopComputers(invoke); + await vi.waitFor(() => expect(computers.snapshot()?.error).toBe("OpenAIDE could not read its paired computers. Try again.")); + expect(computers.snapshot()?.computers).toEqual([]); + fail = false; + const unsubscribe = computers.subscribe(() => undefined); + await vi.waitFor(() => expect(computers.snapshot()).toEqual({ device: "computer", computers: [studio] })); + unsubscribe(); + }); + + it("switches, forgets and pairs through the shell, and rereads the list after forgetting", async () => { + const invoke = invoker({ desktop_remote_snapshot: async () => [studio] }) as ReturnType; + const computers = createDesktopComputers(invoke as never); + await computers.use(studio.id); + await computers.use(); + await computers.forget(studio.id); + await computers.pair!("OAI1 CODE"); + expect(invoke.mock.calls.filter(([command]) => command !== "desktop_remote_snapshot")).toEqual([ + ["desktop_remote_use", { id: studio.id }], + ["desktop_remote_use", { id: null }], + ["desktop_remote_forget", { id: studio.id }], + ["desktop_remote_pair", { code: "OAI1 CODE" }], + ]); + expect(invoke.mock.calls.filter(([command]) => command === "desktop_remote_snapshot")).toHaveLength(2); + }); +}); diff --git a/apps/desktop/frontend/desktopComputers.ts b/apps/desktop/frontend/desktopComputers.ts new file mode 100644 index 00000000..f05d23f3 --- /dev/null +++ b/apps/desktop/frontend/desktopComputers.ts @@ -0,0 +1,44 @@ +import type { invoke as tauriInvoke } from "@tauri-apps/api/core"; +import type { ComputersCapability, ComputersSnapshot, ShellComputer } from "../../../packages/frontend/src/services/computers"; + +/** + * The Desktop's paired computers (ADR-0063), kept by the native side in its + * bootstrap preferences. A switch or a pairing that takes effect restarts the + * Desktop, so their promises settle only when the native side refused. + */ +export function createDesktopComputers(invoke: typeof tauriInvoke): ComputersCapability { + let snapshot: ComputersSnapshot | undefined; + let reading: Promise | undefined; + const listeners = new Set<() => void>(); + const publish = (next: ComputersSnapshot) => { + snapshot = next; + listeners.forEach((listener) => listener()); + }; + const read = () => reading ??= (async () => { + const startedAt = performance.now(); + try { + publish({ device: "computer", computers: await invoke("desktop_remote_snapshot") }); + } catch { + console.warn(`desktop_computers_read_completed outcome=failure duration_ms=${Math.round(performance.now() - startedAt)}`); + publish({ device: "computer", computers: snapshot?.computers ?? [], error: "OpenAIDE could not read its paired computers. Try again." }); + } finally { + reading = undefined; + } + })(); + void read(); + return { + snapshot: () => snapshot, + subscribe(listener) { + listeners.add(listener); + // A failed read is retried when something new starts watching, such as Settings opening. + if (snapshot?.error) void read(); + return () => listeners.delete(listener); + }, + use: (id) => invoke("desktop_remote_use", { id: id ?? null }), + async forget(id) { + await invoke("desktop_remote_forget", { id }); + await read(); + }, + pair: (code) => invoke("desktop_remote_pair", { code }), + }; +} diff --git a/apps/desktop/frontend/desktopShell.ts b/apps/desktop/frontend/desktopShell.ts index 412a7404..f42517d5 100644 --- a/apps/desktop/frontend/desktopShell.ts +++ b/apps/desktop/frontend/desktopShell.ts @@ -13,13 +13,14 @@ import { import { openUrl } from "@tauri-apps/plugin-opener"; import { CLIENT_DETACH, type AppServerSession } from "@openaide/app-server-client"; import type { HostToWebviewMessage, SettingsIntent, SettingsTabId } from "@openaide/app-shell-contracts"; -import type { FrontendShell, RemoteComputer } from "../../../packages/frontend/src/services/frontendShell"; +import type { FrontendShell } from "../../../packages/frontend/src/services/frontendShell"; import { createShellAppearance } from "../../../packages/frontend/src/services/shellAppearance"; import type { PostHostMessage } from "../../../packages/frontend/src/state/postHostMessage"; import type { WebviewBootstrap } from "../../../packages/frontend/src/state/surfaceTypes"; import { quitDesktop } from "./desktopQuitLifecycle"; import { createDesktopProjectPicker } from "./desktopProjectPicker"; import { createDesktopServerRecovery } from "./desktopServerRecovery"; +import { createDesktopComputers } from "./desktopComputers"; import { createDesktopUpdates } from "./desktopUpdates"; import type { DesktopBootstrap, LocalHttpConnection } from "./desktopBootstrap"; import { desktopCommandForKeyboardEvent, type DesktopCommand, type DesktopSurfaceCommand } from "./desktopCommands"; @@ -255,13 +256,7 @@ export function createDesktopShell( }), select: (environment) => invoke("set_desktop_runtime_environment", { environment }), } : undefined, - remoteComputers: { - current: host.remoteComputer, - list: () => invoke("desktop_remote_snapshot"), - pair: (code) => invoke("desktop_remote_pair", { code }), - use: (id) => invoke("desktop_remote_use", { id: id ?? null }), - forget: (id) => invoke("desktop_remote_forget", { id }), - }, + computers: createDesktopComputers(invoke), desktopUpdates, desktopCommands: { subscribe(listener) { diff --git a/apps/desktop/src-tauri/gen/schemas/linux-schema.json b/apps/desktop/src-tauri/gen/schemas/linux-schema.json new file mode 100644 index 00000000..444f38bf --- /dev/null +++ b/apps/desktop/src-tauri/gen/schemas/linux-schema.json @@ -0,0 +1,2879 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + }, + "deny": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-exit", + "markdownDescription": "Enables the exit command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-exit", + "markdownDescription": "Denies the exit command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen-on-monitor", + "markdownDescription": "Enables the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen-on-monitor", + "markdownDescription": "Denies the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "type": "string", + "const": "notification:default", + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + }, + { + "description": "Enables the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-batch", + "markdownDescription": "Enables the batch command without any pre-configured scope." + }, + { + "description": "Enables the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-cancel", + "markdownDescription": "Enables the cancel command without any pre-configured scope." + }, + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-create-channel", + "markdownDescription": "Enables the create_channel command without any pre-configured scope." + }, + { + "description": "Enables the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-delete-channel", + "markdownDescription": "Enables the delete_channel command without any pre-configured scope." + }, + { + "description": "Enables the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-active", + "markdownDescription": "Enables the get_active command without any pre-configured scope." + }, + { + "description": "Enables the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-pending", + "markdownDescription": "Enables the get_pending command without any pre-configured scope." + }, + { + "description": "Enables the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-is-permission-granted", + "markdownDescription": "Enables the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Enables the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-list-channels", + "markdownDescription": "Enables the list_channels command without any pre-configured scope." + }, + { + "description": "Enables the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-notify", + "markdownDescription": "Enables the notify command without any pre-configured scope." + }, + { + "description": "Enables the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-permission-state", + "markdownDescription": "Enables the permission_state command without any pre-configured scope." + }, + { + "description": "Enables the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-action-types", + "markdownDescription": "Enables the register_action_types command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-remove-active", + "markdownDescription": "Enables the remove_active command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-request-permission", + "markdownDescription": "Enables the request_permission command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Denies the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-batch", + "markdownDescription": "Denies the batch command without any pre-configured scope." + }, + { + "description": "Denies the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-cancel", + "markdownDescription": "Denies the cancel command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-create-channel", + "markdownDescription": "Denies the create_channel command without any pre-configured scope." + }, + { + "description": "Denies the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-delete-channel", + "markdownDescription": "Denies the delete_channel command without any pre-configured scope." + }, + { + "description": "Denies the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-active", + "markdownDescription": "Denies the get_active command without any pre-configured scope." + }, + { + "description": "Denies the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-pending", + "markdownDescription": "Denies the get_pending command without any pre-configured scope." + }, + { + "description": "Denies the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-is-permission-granted", + "markdownDescription": "Denies the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Denies the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-list-channels", + "markdownDescription": "Denies the list_channels command without any pre-configured scope." + }, + { + "description": "Denies the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-notify", + "markdownDescription": "Denies the notify command without any pre-configured scope." + }, + { + "description": "Denies the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-permission-state", + "markdownDescription": "Denies the permission_state command without any pre-configured scope." + }, + { + "description": "Denies the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-action-types", + "markdownDescription": "Denies the register_action_types command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-remove-active", + "markdownDescription": "Denies the remove_active command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-request-permission", + "markdownDescription": "Denies the request_permission command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`", + "type": "string", + "const": "updater:default", + "markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`" + }, + { + "description": "Enables the check command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-check", + "markdownDescription": "Enables the check command without any pre-configured scope." + }, + { + "description": "Enables the download command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download", + "markdownDescription": "Enables the download command without any pre-configured scope." + }, + { + "description": "Enables the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download-and-install", + "markdownDescription": "Enables the download_and_install command without any pre-configured scope." + }, + { + "description": "Enables the install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-install", + "markdownDescription": "Enables the install command without any pre-configured scope." + }, + { + "description": "Denies the check command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-check", + "markdownDescription": "Denies the check command without any pre-configured scope." + }, + { + "description": "Denies the download command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download", + "markdownDescription": "Denies the download command without any pre-configured scope." + }, + { + "description": "Denies the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download-and-install", + "markdownDescription": "Denies the download_and_install command without any pre-configured scope." + }, + { + "description": "Denies the install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-install", + "markdownDescription": "Denies the install command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "Application": { + "description": "Opener scope application.", + "anyOf": [ + { + "description": "Open in default application.", + "type": "null" + }, + { + "description": "If true, allow open with any application.", + "type": "boolean" + }, + { + "description": "Allow specific application to open with.", + "type": "string" + } + ] + } + } +} \ No newline at end of file diff --git a/apps/desktop/src-tauri/src/macos_menu.rs b/apps/desktop/src-tauri/src/macos_menu.rs new file mode 100644 index 00000000..e5f29fa7 --- /dev/null +++ b/apps/desktop/src-tauri/src/macos_menu.rs @@ -0,0 +1,51 @@ +use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}; + +/// Installs the native menu. Every item id is a `desktop-command` the shared +/// Frontend handles, so the WebView shortcuts on other platforms stay the same set. +pub(crate) fn install(app: &mut tauri::App) -> tauri::Result<()> { + let new_task = MenuItemBuilder::with_id("new-task", "New Task") + .accelerator("CmdOrCtrl+N") + .build(app)?; + let open_project = MenuItemBuilder::with_id("open-project", "Add Project Folder…") + .accelerator("CmdOrCtrl+O") + .build(app)?; + let settings = MenuItemBuilder::with_id("settings", "Settings…") + .accelerator("CmdOrCtrl+,") + .build(app)?; + let switch_computer = MenuItemBuilder::with_id("switch-computer", "Switch Computer…") + .accelerator("CmdOrCtrl+Shift+S") + .build(app)?; + let check_for_updates = + MenuItemBuilder::with_id("check-for-updates", "Check for Updates…").build(app)?; + let quit = MenuItemBuilder::with_id("quit", "Quit OpenAIDE") + .accelerator("CmdOrCtrl+Q") + .build(app)?; + let application = SubmenuBuilder::new(app, "OpenAIDE") + .items(&[ + &settings, + &check_for_updates, + &PredefinedMenuItem::separator(app)?, + &quit, + ]) + .build()?; + let file = SubmenuBuilder::new(app, "File") + .items(&[&new_task, &open_project, &PredefinedMenuItem::separator(app)?, &switch_computer]) + .build()?; + let edit = SubmenuBuilder::new(app, "Edit") + .items(&[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ]) + .build()?; + app.set_menu( + MenuBuilder::new(app) + .items(&[&application, &file, &edit]) + .build()?, + )?; + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index f036ba7d..ad1db48b 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -13,8 +13,6 @@ use std::thread; use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; -#[cfg(target_os = "macos")] -use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}; use tauri::{Emitter, Manager}; // First, so every module below can use `desktop_log!`. @@ -37,6 +35,8 @@ mod desktop_update_shutdown; mod desktop_update_tests; mod desktop_webview_log; #[cfg(target_os = "macos")] +mod macos_menu; +#[cfg(target_os = "macos")] mod macos_shell_path; #[cfg(target_os = "macos")] mod macos_webview_resize; @@ -182,7 +182,7 @@ fn main() { app.manage(DesktopQuitState::default()); desktop_app_server_hold::install(app); #[cfg(target_os = "macos")] - install_macos_menu(app)?; + macos_menu::install(app)?; #[cfg(target_os = "macos")] configure_macos_webview_resize(app)?; #[cfg(all(target_os = "macos", debug_assertions))] @@ -364,52 +364,6 @@ fn arm_macos_quit_watchdog(app: &tauri::AppHandle) { }); } -#[cfg(target_os = "macos")] -fn install_macos_menu(app: &mut tauri::App) -> tauri::Result<()> { - let new_task = MenuItemBuilder::with_id("new-task", "New Task") - .accelerator("CmdOrCtrl+N") - .build(app)?; - let open_project = MenuItemBuilder::with_id("open-project", "Add Project Folder…") - .accelerator("CmdOrCtrl+O") - .build(app)?; - let settings = MenuItemBuilder::with_id("settings", "Settings…") - .accelerator("CmdOrCtrl+,") - .build(app)?; - let check_for_updates = - MenuItemBuilder::with_id("check-for-updates", "Check for Updates…").build(app)?; - let quit = MenuItemBuilder::with_id("quit", "Quit OpenAIDE") - .accelerator("CmdOrCtrl+Q") - .build(app)?; - let application = SubmenuBuilder::new(app, "OpenAIDE") - .items(&[ - &settings, - &check_for_updates, - &PredefinedMenuItem::separator(app)?, - &quit, - ]) - .build()?; - let file = SubmenuBuilder::new(app, "File") - .items(&[&new_task, &open_project]) - .build()?; - let edit = SubmenuBuilder::new(app, "Edit") - .items(&[ - &PredefinedMenuItem::undo(app, None)?, - &PredefinedMenuItem::redo(app, None)?, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::cut(app, None)?, - &PredefinedMenuItem::copy(app, None)?, - &PredefinedMenuItem::paste(app, None)?, - &PredefinedMenuItem::select_all(app, None)?, - ]) - .build()?; - app.set_menu( - MenuBuilder::new(app) - .items(&[&application, &file, &edit]) - .build()?, - )?; - Ok(()) -} - #[tauri::command] async fn desktop_bootstrap( app: tauri::AppHandle, diff --git a/apps/web/frontend/androidConnectionSettings.test.ts b/apps/web/frontend/androidConnectionSettings.test.ts index bea251ae..80ac2930 100644 --- a/apps/web/frontend/androidConnectionSettings.test.ts +++ b/apps/web/frontend/androidConnectionSettings.test.ts @@ -94,4 +94,30 @@ describe("Android connection controls", () => { test.receive({ type: "result", id: request.id, accepted: false }); await expect(result).rejects.toThrow("unavailable"); }); + it("follows the phone's computers on every page, apart from the Settings state", () => { + const test = harness(); + test.attach(); + const computers = test.capability!.computers; + const changed = vi.fn(); + computers.subscribe(changed); + expect(computers.snapshot()).toBeUndefined(); + test.receive({ type: "computers", computers: [{ id: "studio", name: "Studio", current: true }] }); + expect(computers.snapshot()).toEqual({ device: "phone", computers: [{ id: "studio", name: "Studio", current: true }] }); + expect(computers.snapshot()).toBe(computers.snapshot()); + expect(test.capability!.snapshot()).toBeUndefined(); + expect(changed).toHaveBeenCalledOnce(); + }); + it("switches through the phone's own commands", async () => { + const test = harness(); + test.attach(); + const sent = () => test.port.postMessage.mock.calls.map(([message]) => JSON.parse(message)); + const answer = () => { for (const request of sent()) test.receive({ type: "result", id: request.id, accepted: true }); }; + const toStudio = test.capability!.computers.use("studio"); + answer(); + await toStudio; + const toPhone = test.capability!.computers.use(); + answer(); + await toPhone; + expect(sent().map((request) => request.command)).toEqual([{ action: "use_computer", id: "studio" }, { action: "local" }]); + }); }); diff --git a/apps/web/frontend/androidConnectionSettings.ts b/apps/web/frontend/androidConnectionSettings.ts index 3ab72215..0a2eb6a9 100644 --- a/apps/web/frontend/androidConnectionSettings.ts +++ b/apps/web/frontend/androidConnectionSettings.ts @@ -1,10 +1,18 @@ +import type { ComputersCapability, ComputersSnapshot } from "../../../packages/frontend/src/services/computers"; import type { ConnectionCommand, ConnectionSettings, ConnectionSnapshot } from "../../../packages/frontend/src/services/connectionSettings"; import type { DesktopUpdateCapability } from "../../../packages/frontend/src/services/frontendShell"; import { androidAppUpdates, type AndroidUpdateCommand } from "./androidAppUpdates"; import { androidSystemAppearance } from "./androidSystemAppearance"; -/** Phone controls the Android App serves over one native channel: connection settings and its own updates. */ -export type AndroidConnectionSettings = ConnectionSettings & { appUpdates: DesktopUpdateCapability }; +/** + * The phone's controls over one native channel: Settings state, which the phone + * sends only to its Settings sections; its own updates; and the computers, which + * it sends to every page so the switcher works anywhere (ADR-0070). + */ +export type AndroidConnectionSettings = ConnectionSettings & { + appUpdates: DesktopUpdateCapability; + computers: ComputersCapability; +}; export function androidConnectionSettings(host: Window): AndroidConnectionSettings | undefined { if (!/(?:^| )OpenAIDE-Android\/1(?: |$)/.test(host.navigator.userAgent)) return undefined; @@ -14,6 +22,7 @@ export function androidConnectionSettings(host: Window): AndroidConnectionSettin catch { } }); let state: ConnectionSnapshot | undefined; + let computers: ComputersSnapshot | undefined; let sequence = 0; const listeners = new Set<() => void>(); const pending = new Map(); @@ -37,6 +46,9 @@ export function androidConnectionSettings(host: Window): AndroidConnectionSettin notify(); } else if (message.type === "update") { updates.receive(message.update); + } else if (message.type === "computers" && Array.isArray(message.computers)) { + computers = { device: "phone", computers: message.computers }; + notify(); } } catch { } }; @@ -69,10 +81,18 @@ export function androidConnectionSettings(host: Window): AndroidConnectionSettin request.send(); }); const updates = androidAppUpdates(execute); + const subscribe = (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; return { snapshot: () => state, - subscribe: listener => { listeners.add(listener); return () => { listeners.delete(listener); }; }, + subscribe, execute, appUpdates: updates.capability, + computers: { + snapshot: () => computers, + subscribe, + use: id => execute(id ? { action: "use_computer", id } : { action: "local" }), + forget: id => execute({ action: "forget_computer", id }), + openPairing: () => execute({ action: "pair_setup" }), + }, }; } diff --git a/apps/web/frontend/webAppShell.ts b/apps/web/frontend/webAppShell.ts index ada0cdf7..7487451d 100644 --- a/apps/web/frontend/webAppShell.ts +++ b/apps/web/frontend/webAppShell.ts @@ -102,6 +102,7 @@ export function createWebAppShell(): FrontendShell { return { appearance, connectionSettings, + computers: connectionSettings?.computers, bootstrap, clipboard: { writeText: writeBrowserClipboardText }, // The Android App opens its camera for a capture input; browsers vary, and Chrome already offers one. diff --git a/docs/plans/computer-switcher.md b/docs/plans/computer-switcher.md index ae607aba..7d9a5259 100644 --- a/docs/plans/computer-switcher.md +++ b/docs/plans/computer-switcher.md @@ -16,10 +16,10 @@ Status: agreed design ([ADR-0070](../adr/0070-switching-computers-is-in-place-an ## Slice 1: shared switcher and Mobile App state on every page -- Replace `connectionSettings`' computer fields and `remoteComputers` with one `computers` shell capability: snapshot (this device, computers with id, name, kind, current, link), subscribe, `use(id)`, `cancel()`. Each shell adapts its native side to it. The Settings, Remote page reads the same capability. +- Replace `connectionSettings`' computer fields and `remoteComputers` with one `computers` shell capability: snapshot (this device, computers with id, name, current), subscribe, `use(id)`, `forget(id)`, and the shell's pairing entry. Each shell adapts its native side to it. The Settings, Remote page reads the same capability. The link state comes from the Frontend's own session; `cancel()` arrives with connect-first in slice 3 and `kind` with SSH Computers. - `ComputerSwitcher` in the sidebar footer's Settings row, shown once any computer exists: name and dot on another computer, icon on this device, words only for an unhealthy link. An upward listbox: this device, then computers in the order added, current checked, last item "Pair or manage computers…". Remove `SidebarComputer` and the Desktop title-bar lead. - Ctrl/Cmd+Shift+S opens it (`desktopCommands` gains `switch-computer`, and macOS File ▸ Switch Computer…); arrows, Enter, and 1 to 9 inside. -- Mobile App: push computer state to every page; accept `state`, `use_computer`, and `local` from any route; all other commands stay Settings-only. +- Mobile App: push the computer list (not the full connection state) to every page and on attach or resume; accept `use_computer` and `local` from any route; all other commands stay Settings-only. - Tests: switcher unit and DOM tests (unpaired shows nothing, local icon, remote name, keyboard, narrow truncation); Android bridge policy test for the widened commands; one wide and one narrow viewport check. ## Slice 2: Desktop switches in place and keeps its App Server @@ -50,4 +50,4 @@ Each switch emits `computer_switch` start and terminal events under one correlat ## Open questions -- SSH Computers join the list once ADR-0069 lands; the capability's `kind` and route fields are shaped for them now. +- SSH Computers join the list once ADR-0069 lands, adding a `kind` to each computer and merged routes (ADR-0070). diff --git a/packages/frontend/src/components/AppSurfaces.tsx b/packages/frontend/src/components/AppSurfaces.tsx index e93a78a2..c710310b 100644 --- a/packages/frontend/src/components/AppSurfaces.tsx +++ b/packages/frontend/src/components/AppSurfaces.tsx @@ -16,7 +16,8 @@ import { updateTaskSurfaceTitle } from "../services/hostBridge"; import { ProjectFolderDialog } from "./ProjectFolderDialog"; import { ProjectRemoveDialog } from "./ProjectRemoveDialog"; import { currentFrontendShell, type DesktopUpdateCapability } from "../services/frontendShell"; -import { SidebarComputer, computerLink, useComputerInUse } from "./SidebarComputer"; +import { computerLink } from "./ComputerSwitcher"; +import { useComputers } from "../services/computers"; export function AppSurfaces({ controller }: { controller: AppController }) { useInputModality(); @@ -35,8 +36,9 @@ export function AppSurfaces({ controller }: { controller: AppController }) { const { appServerError, navigation, settings } = view; const frontendShell = currentFrontendShell(); const desktopUpdate = useDesktopUpdateSnapshot(frontendShell?.desktopUpdates); - const computerInUse = useComputerInUse(frontendShell); + const computers = useComputers(frontendShell?.computers); const computerLinkState = computerLink(controller.backendConnectionState); + const [computerSwitcherRequest, setComputerSwitcherRequest] = useState(0); const updateAvailable = desktopUpdate?.kind === "available" || desktopUpdate?.kind === "downloading" || desktopUpdate?.kind === "readyToUpdate"; @@ -248,20 +250,16 @@ export function AppSurfaces({ controller }: { controller: AppController }) { } else if (command === "new-task") callbacks.navigation.openNewTask(); else if (command === "settings") callbacks.navigation.openSettings(); else if (command === "open-project") addProject?.(); + else if (command === "switch-computer") setComputerSwitcherRequest((request) => request + 1); }), [addProject, callbacks.navigation, frontendShell]); const desktopTaskHeaderIntegrated = Boolean(desktopWindow && renderableTaskSnapshot && !openingNativeSession); - const openComputerSettings = () => callbacks.navigation.openSettings(undefined, undefined, undefined, computerInUse?.settingsTab); - // A Desktop window names the computer in its own strip, beside the window - // buttons; other shells give it the first row of the navigation. - const desktopComputer = desktopWindow && computerInUse - ? - : undefined; - const sidebarComputerName = desktopWindow ? undefined : computerInUse?.name; + const openComputerSettings = () => callbacks.navigation.openSettings(undefined, undefined, undefined, "remote"); + const useComputer = frontendShell?.computers ? frontendShell.computers.use : undefined; const desktopTitleBar = desktopWindow - ? + ? : undefined; const desktopSettingsTitleBar = desktopWindow ? ( - + ) : undefined; // Desktop chrome overlays product content so native controls never consume a // separate blank row. Active Tasks provide the draggable product header. @@ -447,8 +445,10 @@ export function AppSurfaces({ controller }: { controller: AppController }) { searchQuery={navigation.searchQuery} settingsStatus={updateAvailable ? "Update available" : undefined} computerLink={computerLinkState} - computerName={sidebarComputerName} - onComputer={openComputerSettings} + computers={computers} + computerSwitcherRequest={computerSwitcherRequest} + onManageComputers={openComputerSettings} + onUseComputer={useComputer} showArchived={navigation.showArchived} taskListError={navigation.taskListError} tasks={visibleTasks} @@ -586,8 +586,10 @@ export function AppSurfaces({ controller }: { controller: AppController }) { searchQuery={navigation.searchQuery} settingsStatus={updateAvailable ? "Update available" : undefined} computerLink={computerLinkState} - computerName={sidebarComputerName} - onComputer={closeAfter(openComputerSettings)} + computers={computers} + computerSwitcherRequest={computerSwitcherRequest} + onManageComputers={closeAfter(openComputerSettings)} + onUseComputer={useComputer} showArchived={navigation.showArchived} taskListError={navigation.taskListError} tasks={visibleTasks} diff --git a/packages/frontend/src/components/ComputerSwitcher.dom.test.tsx b/packages/frontend/src/components/ComputerSwitcher.dom.test.tsx new file mode 100644 index 00000000..30d424f3 --- /dev/null +++ b/packages/frontend/src/components/ComputerSwitcher.dom.test.tsx @@ -0,0 +1,164 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ComputersSnapshot } from "../services/computers"; +import { ComputerSwitcher, computerLink, type ComputerLink } from "./ComputerSwitcher"; + +const studio = { id: "studio-key", name: "Studio", current: false }; +const laptop = { id: "laptop-key", name: "", current: false }; + +describe("ComputerSwitcher", () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + vi.spyOn(console, "info").mockImplementation(() => undefined); + vi.spyOn(console, "warn").mockImplementation(() => undefined); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + vi.restoreAllMocks(); + }); + + function render( + snapshot: ComputersSnapshot | undefined, + { + link = "connected" as ComputerLink, + onUse = vi.fn(async (_id?: string): Promise => undefined), + onManage = vi.fn(), + openRequest = 0, + } = {}, + ) { + act(() => root.render( + , + )); + return { onUse, onManage }; + } + + const trigger = () => container.querySelector("button.computer-switcher-trigger"); + const menuItems = () => Array.from(document.querySelectorAll('[role="menu"] [role^="menuitem"]')); + const open = () => act(() => trigger()!.click()); + const isOpen = () => trigger()!.getAttribute("aria-expanded") === "true"; + // Keys reach the list from the focused row, as arrow navigation leaves them. + const press = (key: string) => act(() => { + menuItems()[0].dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key })); + }); + + it("stays out of the way until a computer is paired", () => { + render(undefined); + expect(trigger()).toBeNull(); + render({ device: "computer", computers: [] }); + expect(trigger()).toBeNull(); + }); + + it("shows only an icon while this device is in use", () => { + render({ device: "computer", computers: [studio] }); + expect(trigger()!.getAttribute("aria-label")).toBe("Working on this computer. Change computer"); + expect(trigger()!.textContent).toBe(""); + }); + + it("names the computer in use, with words only for an unhealthy link", () => { + render({ device: "computer", computers: [{ ...studio, current: true }] }); + expect(trigger()!.textContent).toBe("Studio"); + render({ device: "computer", computers: [{ ...studio, current: true }] }, { link: "reconnecting" }); + expect(trigger()!.textContent).toBe("StudioReconnecting…"); + expect(trigger()!.getAttribute("aria-label")).toBe("Working on Studio. Reconnecting…. Change computer"); + }); + + it("lists this device first, then computers in the order added, with the one in use checked", () => { + render({ device: "phone", computers: [{ ...studio, current: true }, laptop] }); + open(); + expect(menuItems().map((item) => [item.textContent, item.getAttribute("aria-checked")])).toEqual([ + ["1This phone", "false"], + ["2Studio", "true"], + ["3Remote computer", "false"], + ["Pair or manage computers…", null], + ]); + }); + + it("asks the shell to switch and shows the switch in progress", async () => { + let accept!: () => void; + const onUse = vi.fn(() => new Promise((resolve) => { accept = resolve; })); + render({ device: "computer", computers: [studio] }, { onUse }); + open(); + act(() => menuItems()[1].click()); + expect(onUse).toHaveBeenCalledWith("studio-key"); + expect(trigger()!.getAttribute("aria-busy")).toBe("true"); + open(); + expect(menuItems()[1].textContent).toContain("Connecting…"); + expect(menuItems().every((item) => item.getAttribute("aria-disabled") === "true" || item.textContent === "Pair or manage computers…")).toBe(true); + await act(async () => accept()); + }); + + it("switches back to this device without an id", () => { + const { onUse } = render({ device: "computer", computers: [{ ...studio, current: true }] }); + open(); + act(() => menuItems()[0].click()); + expect(onUse).toHaveBeenCalledWith(undefined); + }); + + it("closes without switching when the computer in use is chosen", () => { + const { onUse } = render({ device: "computer", computers: [{ ...studio, current: true }] }); + open(); + act(() => menuItems()[1].click()); + expect(onUse).not.toHaveBeenCalled(); + expect(isOpen()).toBe(false); + }); + + it("keeps the user in place with the shell's sentence when the switch is refused", async () => { + const onUse = vi.fn(async () => { throw new Error("Studio could not be reached."); }); + render({ device: "computer", computers: [studio] }, { onUse }); + open(); + await act(async () => menuItems()[1].click()); + expect(document.querySelector('[role="menu"] [role="alert"]')!.textContent).toBe("Studio could not be reached."); + expect(trigger()!.getAttribute("aria-busy")).toBeNull(); + await act(async () => menuItems()[1].click()); + expect(onUse).toHaveBeenCalledTimes(2); + }); + + it("picks a row by its number", () => { + const { onUse } = render({ device: "computer", computers: [studio, laptop] }); + open(); + press("3"); + expect(onUse).toHaveBeenCalledWith("laptop-key"); + }); + + it("ignores numbers without a row", () => { + const { onUse } = render({ device: "computer", computers: [studio] }); + open(); + press("5"); + expect(onUse).not.toHaveBeenCalled(); + }); + + it("leads to Settings to pair or manage", () => { + const { onManage } = render({ device: "computer", computers: [studio] }); + open(); + act(() => menuItems().at(-1)!.click()); + expect(onManage).toHaveBeenCalledOnce(); + expect(isOpen()).toBe(false); + }); + + it("opens when a shell command asks for it", () => { + const snapshot: ComputersSnapshot = { device: "computer", computers: [studio] }; + render(snapshot, { openRequest: 0 }); + expect(isOpen()).toBe(false); + render(snapshot, { openRequest: 1 }); + expect(isOpen()).toBe(true); + }); + + it("reads the link from the session state", () => { + expect(computerLink({ status: "ready" })).toBe("connected"); + expect(computerLink({ status: "ready", unresponsive: true })).toBe("notResponding"); + expect(computerLink({ status: "connecting" })).toBe("connecting"); + expect(computerLink({ status: "reconnecting", message: "" })).toBe("reconnecting"); + expect(computerLink({ status: "unavailable", message: "" })).toBe("notConnected"); + }); +}); diff --git a/packages/frontend/src/components/ComputerSwitcher.tsx b/packages/frontend/src/components/ComputerSwitcher.tsx new file mode 100644 index 00000000..4b4cdbd8 --- /dev/null +++ b/packages/frontend/src/components/ComputerSwitcher.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState, type KeyboardEvent } from "react"; +import { Check, Monitor, MonitorSmartphone } from "lucide-react"; +import { computerName, currentComputer, type ComputersSnapshot } from "../services/computers"; +import type { BackendConnectionState } from "./appControllerBackendLifecycle"; +import { PopupMenu } from "./Popup"; +import { errorSentence } from "./settings/settingsKit"; + +export type ComputerLink = "connected" | "connecting" | "reconnecting" | "notResponding" | "notConnected"; + +const linkLabels: Record = { + connected: "Connected", + connecting: "Connecting…", + reconnecting: "Reconnecting…", + notResponding: "Not responding", + notConnected: "Not connected", +}; + +export function computerLink(state: BackendConnectionState): ComputerLink { + switch (state.status) { + case "ready": + return state.unresponsive ? "notResponding" : "connected"; + case "connecting": + return "connecting"; + case "reconnecting": + return "reconnecting"; + case "unavailable": + return "notConnected"; + } +} + +/** "" stands for this device in the pending and failed switch. */ +type Row = { id: string; label: string; current: boolean }; + +/** + * Switches the computer in use in place (ADR-0070). It appears only once + * another computer exists, so a user who never pairs sees nothing new. The + * trigger names the other computer in use, with the dot alone saying the link + * is healthy; this device needs only the icon. + */ +export function ComputerSwitcher({ link, onManage, onUse, openRequest = 0, snapshot }: { + link: ComputerLink; + onManage: () => void; + onUse: (id?: string) => Promise; + /** Increments when a shell command asks to open the list. */ + openRequest?: number; + snapshot: ComputersSnapshot | undefined; +}) { + const [open, setOpen] = useState(false); + const [pending, setPending] = useState(); + const [failed, setFailed] = useState<{ id: string; message: string }>(); + useEffect(() => { + if (openRequest > 0) setOpen(true); + }, [openRequest]); + if (!snapshot?.computers.length) return null; + + const inUse = currentComputer(snapshot); + const here = snapshot.device === "phone" ? "This phone" : "This computer"; + const rows: Row[] = [ + { id: "", label: here, current: !inUse }, + ...snapshot.computers.map((computer) => ({ id: computer.id, label: computerName(computer), current: computer.current })), + ]; + const label = linkLabels[link]; + const triggerLabel = inUse + ? `Working on ${computerName(inUse)}. ${label}. Change computer` + : `Working on ${here.toLowerCase()}. Change computer`; + + const choose = (row: Row) => { + if (pending !== undefined) return; + if (row.current) { + setOpen(false); + return; + } + setFailed(undefined); + setPending(row.id); + void requestSwitch(onUse, row.id || undefined).catch((cause: unknown) => { + setPending(undefined); + setFailed({ id: row.id, message: errorSentence(cause, "OpenAIDE could not switch. Try again.") }); + }); + }; + const pickByNumber = (event: KeyboardEvent) => { + if (event.altKey || event.ctrlKey || event.metaKey || !/^[1-9]$/.test(event.key)) return; + const row = rows[Number(event.key) - 1]; + if (!row) return; + event.preventDefault(); + choose(row); + }; + + return ( + ( + + )} + > +
+ {rows.map((row, index) => ( + + ))} + {failed ?

{failed.message}

: null} + +
+
+ ); +} + +/** + * Logs the switch request at the Frontend boundary. Acceptance is terminal + * here: the shell then replaces the page, and its own events carry the rest. + */ +async function requestSwitch(onUse: (id?: string) => Promise, id: string | undefined) { + const operationId = crypto.randomUUID(); + const startedAt = performance.now(); + const target = id ? "computer" : "this_device"; + console.info(`computer_switch_started operation_id=${operationId} target=${target}`); + try { + await onUse(id); + console.info(`computer_switch_completed operation_id=${operationId} target=${target} outcome=accepted duration_ms=${Math.round(performance.now() - startedAt)}`); + } catch (cause) { + console.warn(`computer_switch_completed operation_id=${operationId} target=${target} outcome=refused duration_ms=${Math.round(performance.now() - startedAt)}`); + throw cause; + } +} diff --git a/packages/frontend/src/components/DesktopTitleBar.tsx b/packages/frontend/src/components/DesktopTitleBar.tsx index d87d74ec..e23e46fa 100644 --- a/packages/frontend/src/components/DesktopTitleBar.tsx +++ b/packages/frontend/src/components/DesktopTitleBar.tsx @@ -1,16 +1,13 @@ -import { type MouseEvent, type ReactNode } from "react"; +import { type MouseEvent } from "react"; import { Minus, Square, X } from "lucide-react"; import type { DesktopWindowCapability } from "../services/frontendShell"; /** Renders product content inside shell-owned native window chrome. */ export function DesktopTitleBar({ integrated = false, - sidebarLead, window: desktopWindow, }: { integrated?: boolean; - /** Shown in the strip above the navigation, beside the native window buttons. */ - sidebarLead?: ReactNode; window: DesktopWindowCapability; }) { const windowsChrome = desktopWindow.platform === "windows"; @@ -25,7 +22,7 @@ export function DesktopTitleBar({ integrated ? "desktop-title-bar-integrated" : undefined, ].filter(Boolean).join(" ")} > -
{sidebarLead}
+
{windowsChrome && !integrated ? OpenAIDE : null}
diff --git a/packages/frontend/src/components/Sidebar.tsx b/packages/frontend/src/components/Sidebar.tsx index 8e37ebc4..9aca7d9b 100644 --- a/packages/frontend/src/components/Sidebar.tsx +++ b/packages/frontend/src/components/Sidebar.tsx @@ -18,7 +18,8 @@ import { useScrollOverflow } from "./useScrollOverflow"; import { WorkspaceSetupPrompt } from "./WorkspaceSetupPrompt"; import { AgentDisableDialog } from "./AgentDisableDialog"; import { useAgentDisableConfirmation } from "./useAgentDisableConfirmation"; -import { SidebarComputer, type ComputerLink } from "./SidebarComputer"; +import { ComputerSwitcher, type ComputerLink } from "./ComputerSwitcher"; +import type { ComputersSnapshot } from "../services/computers"; import type { AgentDisableOutcome } from "../intents/agentSettingsIntents"; import { agentIntegrationInstallingLabel } from "./agentActivityPresentation"; @@ -73,10 +74,13 @@ type SidebarProps = { searchQuery: string; settingsStatus?: string; settingsActive?: boolean; - /** The other computer in use; absent while this device does the work or the window strip names it. */ - computerName?: string; + /** The shell's computers; the switcher appears once one other than this device exists. */ + computers?: ComputersSnapshot; computerLink?: ComputerLink; - onComputer?: () => void; + /** Increments when a shell command asks to open the computer list. */ + computerSwitcherRequest?: number; + onUseComputer?: (id?: string) => Promise; + onManageComputers?: () => void; showArchived: boolean; taskListError?: string; tasks: TaskSummary[]; @@ -130,9 +134,11 @@ export const Sidebar = memo(function Sidebar({ onToggleArchived, searchQuery, settingsStatus, - computerName, + computers, computerLink, - onComputer, + computerSwitcherRequest, + onUseComputer, + onManageComputers, settingsActive = false, showArchived, taskListError, @@ -222,9 +228,6 @@ export const Sidebar = memo(function Sidebar({
) : null}
- {computerName && onComputer && !showArchived ? ( - - ) : null} {!showArchived ? ) : null} - +
+ + {onUseComputer && onManageComputers ? ( + + ) : null} +
{pendingDisable ? ( { - (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; -}); - -function inUse(shell: Partial) { - let seen: ComputerInUse | undefined; - function Probe() { - seen = useComputerInUse(shell as FrontendShell); - return null; - } - act(() => { create(); }); - return { seen: () => seen }; -} - -function phone(initial: Partial) { - let snapshot = initial as ConnectionSnapshot; - const listeners = new Set<() => void>(); - return { - capability: { - snapshot: () => snapshot, - subscribe: (listener: () => void) => { listeners.add(listener); return () => listeners.delete(listener); }, - execute: vi.fn(), - }, - set(next: Partial) { - snapshot = next as ConnectionSnapshot; - listeners.forEach((listener) => listener()); - }, - }; -} - -it("names no computer while this device does the work", () => { - expect(inUse({}).seen()).toBeUndefined(); - expect(inUse({ remoteComputers: { list: vi.fn(), pair: vi.fn(), use: vi.fn(), forget: vi.fn() } }).seen()).toBeUndefined(); - expect(inUse({ connectionSettings: phone({ remote: false, computer: "Studio" }).capability }).seen()).toBeUndefined(); -}); - -it("names the Desktop's paired computer and its Settings tab", () => { - const shell = { remoteComputers: { current: "Studio", list: vi.fn(), pair: vi.fn(), use: vi.fn(), forget: vi.fn() } }; - expect(inUse(shell).seen()).toEqual({ name: "Studio", settingsTab: "remote" }); -}); - -it("follows the phone's choice of computer as it changes", () => { - const source = phone({ remote: true, computer: "Studio" }); - const probe = inUse({ connectionSettings: source.capability }); - expect(probe.seen()).toEqual({ name: "Studio", settingsTab: "remote" }); - act(() => source.set({ remote: true, computer: "" })); - expect(probe.seen()).toEqual({ name: "Another computer", settingsTab: "remote" }); - act(() => source.set({ remote: false, computer: "" })); - expect(probe.seen()).toBeUndefined(); -}); - -it("reads the link from the session state", () => { - expect(computerLink({ status: "ready" })).toBe("connected"); - expect(computerLink({ status: "ready", unresponsive: true })).toBe("notResponding"); - expect(computerLink({ status: "connecting" })).toBe("connecting"); - expect(computerLink({ status: "reconnecting", message: "" })).toBe("reconnecting"); - expect(computerLink({ status: "unavailable", message: "" })).toBe("notConnected"); -}); - -it("shows the computer and its link, and opens the place that changes it", () => { - const onOpen = vi.fn(); - let tree!: ReturnType; - act(() => { tree = create(); }); - const button = tree.root.findByType("button"); - expect(button.props["data-link"]).toBe("reconnecting"); - expect(button.props["aria-label"]).toBe("Working on Studio. Reconnecting…. Change computer"); - expect(tree.root.findByProps({ className: "sidebar-computer-link" }).children).toEqual(["Reconnecting…"]); - act(() => button.props.onClick()); - expect(onOpen).toHaveBeenCalledOnce(); -}); - -it("lets the dot alone speak for a healthy link", () => { - let tree!: ReturnType; - act(() => { tree = create(); }); - expect(tree.root.findAllByProps({ className: "sidebar-computer-link" })).toHaveLength(0); - expect(tree.root.findAllByProps({ className: "sidebar-computer-dot" })).toHaveLength(1); -}); diff --git a/packages/frontend/src/components/SidebarComputer.tsx b/packages/frontend/src/components/SidebarComputer.tsx deleted file mode 100644 index f4f5bdda..00000000 --- a/packages/frontend/src/components/SidebarComputer.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useSyncExternalStore } from "react"; -import { MonitorSmartphone } from "lucide-react"; -import type { SettingsTabId } from "@openaide/app-shell-contracts"; -import type { FrontendShell } from "../services/frontendShell"; -import type { BackendConnectionState } from "./appControllerBackendLifecycle"; - -export type ComputerLink = "connected" | "connecting" | "reconnecting" | "notResponding" | "notConnected"; - -/** The other computer this shell works on, and the Settings tab that changes it. */ -export type ComputerInUse = { name: string; settingsTab: SettingsTabId }; - -const linkLabels: Record = { - connected: "Connected", - connecting: "Connecting…", - reconnecting: "Reconnecting…", - notResponding: "Not responding", - notConnected: "Not connected", -}; - -const noSubscription = () => () => {}; -const noSnapshot = () => undefined; - -/** - * Names the other computer only while one is in use; working on this device - * needs no indicator. The phone's choice changes live, the Desktop's is fixed - * for the shell's run. - */ -export function useComputerInUse(shell: FrontendShell | undefined): ComputerInUse | undefined { - const phone = shell?.connectionSettings; - const snapshot = useSyncExternalStore( - phone?.subscribe ?? noSubscription, - phone?.snapshot ?? noSnapshot, - phone?.snapshot ?? noSnapshot, - ); - if (snapshot?.remote) { - return { name: snapshot.computer || "Another computer", settingsTab: "remote" }; - } - const desktop = shell?.remoteComputers?.current; - return desktop ? { name: desktop, settingsTab: "remote" } : undefined; -} - -export function computerLink(state: BackendConnectionState): ComputerLink { - switch (state.status) { - case "ready": - return state.unresponsive ? "notResponding" : "connected"; - case "connecting": - return "connecting"; - case "reconnecting": - return "reconnecting"; - case "unavailable": - return "notConnected"; - } -} - -/** - * Heads the navigation with the computer its projects and tasks live on. The - * dot alone says the link is healthy; words appear only when it is not. - */ -export function SidebarComputer({ link, name, onOpen }: { - link: ComputerLink; - name: string; - onOpen: () => void; -}) { - const label = linkLabels[link]; - return ( - - ); -} diff --git a/packages/frontend/src/components/settings/GeneralSection.tsx b/packages/frontend/src/components/settings/GeneralSection.tsx index c5d21233..d56d65fb 100644 --- a/packages/frontend/src/components/settings/GeneralSection.tsx +++ b/packages/frontend/src/components/settings/GeneralSection.tsx @@ -11,6 +11,7 @@ import { } from "../../services/frontendShell"; import { usesMobileComposerBehavior } from "../mobileComposerBehavior"; import type { PhoneConnection } from "./phoneConnection"; +import { currentComputer } from "../../services/computers"; import { settingsInfo } from "./settingsInfo"; import { SettingsButton, SettingsChoice, SettingsInlineError, SettingsKeys, SettingsNotice, SettingsSelect, SettingsSwitch } from "./settingsKit"; import type { SettingsGroup, SettingsRow, SettingsSection } from "./settingsModel"; @@ -36,7 +37,7 @@ export function useGeneralSection({ }: GeneralSectionProps): SettingsSection { const shell = currentFrontendShell(); // While a paired computer is in use, its own runtime applies and this choice has no effect. - const runtime = useRuntimeGroup(shell?.remoteComputers?.current ? undefined : shell?.desktopRuntime); + const runtime = useRuntimeGroup(currentComputer(shell?.computers?.snapshot()) ? undefined : shell?.desktopRuntime); const updates = useUpdateGroup(shell?.desktopUpdates); const enterSends = preferences.composer_submit_shortcut === "enter"; const everyday: SettingsRow[] = []; diff --git a/packages/frontend/src/components/settings/RemoteSection.tsx b/packages/frontend/src/components/settings/RemoteSection.tsx index add1036e..14aa4f50 100644 --- a/packages/frontend/src/components/settings/RemoteSection.tsx +++ b/packages/frontend/src/components/settings/RemoteSection.tsx @@ -2,8 +2,8 @@ import type { DeviceCollectionSnapshot, RemoteDeviceSummary } from "@openaide/ap import { Laptop, Monitor, Plus, QrCode, Smartphone, Wifi } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { RemoteDeviceIntents } from "../../intents/remoteDeviceIntents"; -import { currentFrontendShell, type RemoteComputer, type RemoteComputersCapability } from "../../services/frontendShell"; -import type { ConnectionComputer } from "../../services/connectionSettings"; +import { computerName, currentComputer, useComputers, type ComputersCapability, type ComputersSnapshot, type ShellComputer } from "../../services/computers"; +import { currentFrontendShell } from "../../services/frontendShell"; import { PopupDialog } from "../Popup"; import { InviteDialog, JoinDialog } from "./devicePairingDialogs"; import type { PhoneConnection } from "./phoneConnection"; @@ -19,8 +19,7 @@ export type RemoteSectionProps = { type Dialog = | { kind: "pair" } - | { kind: "forget"; computer: RemoteComputer } - | { kind: "forget-phone"; computer: ConnectionComputer } + | { kind: "forget"; computer: ShellComputer } | { kind: "invite" } | { kind: "join" } | { kind: "remove"; device: RemoteDeviceSummary }; @@ -30,18 +29,24 @@ type Dialog = * connected to, and which devices may connect to that computer. */ export function useRemoteSection({ deviceIntents, devices, phone }: RemoteSectionProps): SettingsSection | undefined { - const capability = currentFrontendShell()?.remoteComputers; - const desktop = useRemoteComputers(capability); + const capability = currentFrontendShell()?.computers; + const computers = useSettingsComputers(capability); const [dialog, setDialog] = useState(); const close = () => setDialog(undefined); - if (!capability && !phone && !deviceIntents) return undefined; + if (!capability && !deviceIntents) return undefined; - const remoteName = capability?.current ?? (phone?.state?.remote ? phone.state.computer || "Remote computer" : undefined); + const inUse = currentComputer(computers.snapshot); + const remoteName = inUse ? computerName(inUse) : undefined; + const device = computers.snapshot?.device === "phone" || phone ? "phone" : "computer"; + const here = `this ${device}`; const groups: SettingsGroup[] = []; - if (capability) groups.push(desktopComputersGroup(desktop, () => setDialog({ kind: "pair" }), (computer) => setDialog({ kind: "forget", computer }))); - else if (phone) groups.push(phoneComputersGroup(phone, (computer) => setDialog({ kind: "forget-phone", computer }))); + if (capability) { + groups.push(computersGroup(computers, phone, () => { + if (capability.pair) setDialog({ kind: "pair" }); + else void computers.openPairing(); + }, (computer) => setDialog({ kind: "forget", computer }))); + } if (deviceIntents) { - const here = phone ? "this phone" : "this computer"; // The App Server names itself; a browser tab on another machine is not "this computer". groups.push(devicesGroup(devices, devices?.serverName || remoteName || here, (next) => setDialog(next))); } @@ -54,27 +59,16 @@ export function useRemoteSection({ deviceIntents, devices, phone }: RemoteSectio groups, overlay: dialog ? ( <> - {dialog.kind === "pair" && capability ? : null} - {dialog.kind === "forget" && capability ? ( - desktop.forget(dialog.computer.id)} - pendingLabel="Forgetting…" - title={`Forget ${dialog.computer.name}?`} - /> - ) : null} - {dialog.kind === "forget-phone" && phone ? ( + {dialog.kind === "pair" && capability?.pair ? : null} + {dialog.kind === "forget" ? ( phone.execute({ action: "forget_computer", id: dialog.computer.id })} + onConfirm={() => computers.forget(dialog.computer.id)} pendingLabel="Forgetting…" - title={`Forget ${dialog.computer.name || "this computer"}?`} + title={`Forget ${computerName(dialog.computer)}?`} /> ) : null} {dialog.kind === "invite" && deviceIntents ? : null} @@ -95,59 +89,44 @@ export function useRemoteSection({ deviceIntents, devices, phone }: RemoteSectio }; } -type DesktopComputers = { - computers: RemoteComputer[] | undefined; - /** Id of the computer being switched to; "" stands for this computer. */ +type SettingsComputers = { + snapshot: ComputersSnapshot | undefined; + /** Id of the computer being switched to; "" stands for this device. */ switching: string | undefined; failed: { id: string; message: string } | undefined; - loadError: string; use(id?: string): Promise; forget(id: string): Promise; + openPairing(): Promise; }; /** - * The shell owns the paired computers and restarts to apply a switch, so a - * settled promise means nothing changed or the shell refused with a sentence. + * The shell owns the computers and replaces the page to apply a switch, so a + * settled `use` means the shell accepted it or refused with a sentence. */ -function useRemoteComputers(capability: RemoteComputersCapability | undefined): DesktopComputers { - const [computers, setComputers] = useState(); +function useSettingsComputers(capability: ComputersCapability | undefined): SettingsComputers { + const snapshot = useComputers(capability); const [switching, setSwitching] = useState(); const [failed, setFailed] = useState<{ id: string; message: string }>(); - const [loadError, setLoadError] = useState(""); const mounted = useRef(true); - const refresh = async () => { - if (!capability) return; - try { - const next = await capability.list(); - if (mounted.current) { - setComputers(next); - setLoadError(""); - } - } catch { - if (mounted.current) setLoadError("OpenAIDE could not read its paired computers. Reopen Settings to try again."); - } - }; useEffect(() => { mounted.current = true; - void refresh(); return () => { mounted.current = false; }; - }, [capability]); + }, []); return { - computers, + snapshot, switching, failed, - loadError, use: async (id) => { if (!capability || switching !== undefined) return; setFailed(undefined); setSwitching(id ?? ""); try { await capability.use(id); - await refresh(); } catch (cause) { - if (mounted.current) setFailed({ id: id ?? "", message: errorSentence(cause, "OpenAIDE could not switch. Try again.") }); - } finally { - if (mounted.current) setSwitching(undefined); + if (mounted.current) { + setSwitching(undefined); + setFailed({ id: id ?? "", message: errorSentence(cause, "OpenAIDE could not switch. Try again.") }); + } } }, forget: async (id) => { @@ -157,121 +136,93 @@ function useRemoteComputers(capability: RemoteComputersCapability | undefined): } catch (cause) { throw new Error(errorSentence(cause, "OpenAIDE could not forget that computer. Try again.")); } - await refresh(); + }, + openPairing: async () => { + try { + await capability?.openPairing?.(); + } catch (cause) { + if (mounted.current) setFailed({ id: "pair", message: errorSentence(cause, "Pairing could not open. Try again.") }); + } }, }; } -function desktopComputersGroup( - desktop: DesktopComputers, +function computersGroup( + computers: SettingsComputers, + phone: PhoneConnection | undefined, onPair: () => void, - onForget: (computer: RemoteComputer) => void, + onForget: (computer: ShellComputer) => void, ): SettingsGroup { - const computers = desktop.computers ?? []; - const remote = computers.some((computer) => computer.current); - const busy = desktop.switching !== undefined; - const rows: SettingsRow[] = [{ - id: "this-computer", - label: "This computer", - description: "Projects, tools and Agents here", - keywords: "computer remote connect switch pc local", - leading: