From 703c80a58abc1fd7b1d4aad96fd4130295bdc80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 29 Aug 2026 10:10:34 +0200 Subject: [PATCH 1/3] feat(browser): add autonomous native previews Expose session-scoped browser open, navigate, snapshot, click, type, and screenshot actions through the existing authenticated CodeNomad automation adapter. Reuse the shared native request transport and session ownership fencing from Developer Automation instead of restoring a separate plugin or route. Implement isolated persistent browser previews in Electron and Windows Tauri with hardened popup, permission, download, navigation, deadline, and stale-reference handling. Keep preview registration and open claims scoped to the owning CodeNomad session across windows. Add the session preview UI, native adapters, localized labels, browser-specific architecture documentation, and focused Electron, Tauri, server, and UI coverage. Developer Automation launch, CDP feedback, and lifecycle code remain in DEV-v2 and are not duplicated here. --- .../codenomad-architecture-guide/SKILL.md | 10 +- .../references/server-conventions.md | 4 +- dev-docs/BROWSER_AUTOMATION.md | 30 + dev-docs/DEVELOPER_AUTOMATION.md | 2 +- dev-docs/INDEX.md | 4 + dev-docs/architecture.md | 5 +- .../electron/main/browser-controller.test.ts | 282 ++++ .../electron/main/browser-controller.ts | 393 +++++ .../main/browser-webview-security.test.ts | 24 + .../electron/main/browser-webview-security.ts | 30 + packages/electron-app/electron/main/ipc.ts | 26 + packages/electron-app/electron/main/main.ts | 46 +- .../electron-app/electron/main/permissions.ts | 6 + .../electron-app/electron/preload/index.cjs | 8 + .../electron/preload/index.test.ts | 1 + packages/electron-app/package.json | 2 +- .../src/opencode/automation-plugin.test.ts | 19 +- .../server/src/opencode/automation-plugin.ts | 83 +- .../server/routes/automation-plugin.test.ts | 41 + .../src/server/routes/automation-plugin.ts | 28 +- packages/tauri-app/src-tauri/Cargo.toml | 2 +- packages/tauri-app/src-tauri/build.rs | 5 + .../src-tauri/capabilities/main-window.json | 7 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/capabilities.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 60 + .../src-tauri/gen/schemas/windows-schema.json | 60 + .../autogenerated/browser_target_action.toml | 11 + .../browser_target_claim_open.toml | 11 + .../browser_target_register.toml | 11 + .../browser_target_unregister.toml | 11 + .../autogenerated/browser_target_update.toml | 11 + .../src-tauri/src/browser_controller.rs | 1324 +++++++++++++++++ packages/tauri-app/src-tauri/src/main.rs | 102 +- packages/ui/src/App.tsx | 31 + .../components/browser-frame-security.test.ts | 22 +- .../src/components/browser-frame-security.ts | 25 +- packages/ui/src/components/browser-frame.tsx | 374 ++++- .../src/components/session-preview-view.tsx | 2 + .../ui/src/lib/i18n/messages/de/session.ts | 1 + .../ui/src/lib/i18n/messages/en/session.ts | 1 + .../ui/src/lib/i18n/messages/es/session.ts | 1 + .../ui/src/lib/i18n/messages/fr/session.ts | 1 + .../ui/src/lib/i18n/messages/he/session.ts | 1 + .../ui/src/lib/i18n/messages/ja/session.ts | 1 + .../ui/src/lib/i18n/messages/ne/session.ts | 1 + .../ui/src/lib/i18n/messages/ru/session.ts | 1 + .../ui/src/lib/i18n/messages/tr/session.ts | 1 + .../src/lib/i18n/messages/zh-Hans/session.ts | 1 + packages/ui/src/lib/native/browser.test.ts | 21 + packages/ui/src/lib/native/browser.ts | 79 + packages/ui/src/types/electron-webview.d.ts | 24 + packages/ui/src/types/global.d.ts | 4 + 53 files changed, 3207 insertions(+), 48 deletions(-) create mode 100644 dev-docs/BROWSER_AUTOMATION.md create mode 100644 packages/electron-app/electron/main/browser-controller.test.ts create mode 100644 packages/electron-app/electron/main/browser-controller.ts create mode 100644 packages/electron-app/electron/main/browser-webview-security.test.ts create mode 100644 packages/electron-app/electron/main/browser-webview-security.ts create mode 100644 packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_action.toml create mode 100644 packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_claim_open.toml create mode 100644 packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_register.toml create mode 100644 packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_unregister.toml create mode 100644 packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_update.toml create mode 100644 packages/tauri-app/src-tauri/src/browser_controller.rs create mode 100644 packages/ui/src/lib/native/browser.test.ts create mode 100644 packages/ui/src/lib/native/browser.ts create mode 100644 packages/ui/src/types/electron-webview.d.ts diff --git a/.opencode/skills/codenomad-architecture-guide/SKILL.md b/.opencode/skills/codenomad-architecture-guide/SKILL.md index 288d692fa..f49404a1f 100644 --- a/.opencode/skills/codenomad-architecture-guide/SKILL.md +++ b/.opencode/skills/codenomad-architecture-guide/SKILL.md @@ -13,19 +13,20 @@ description: | - OpenCode: read the three `sdk-*.md` references before changing client calls or service lifecycle. - Desktop: read `references/desktop-conventions.md`. - Developer Automation: read `../../../dev-docs/DEVELOPER_AUTOMATION.md`. +- Browser automation: read `../../../dev-docs/BROWSER_AUTOMATION.md`. ## Native OpenCode V2 Baseline - The only OpenCode client dependency is the experimental `@opencode-ai/client@beta` protocol. Server and UI follow that dependency together; refresh the client lock before API audits or release validation. The runtime CLI is managed independently and startup has no exact version gate. The public `@opencode-ai/sdk` describes an alternative embedded host. - Do not use `@opencode-ai/sdk`, `@opencode-ai/sdk/v2/client`, or `createOpencodeClient()`; follow installed `@opencode-ai/client` declarations. -- There is no legacy `packages/opencode-plugin/`. Do not restore the V1 compatibility runtime or add general plugin extension points. The reviewed Developer Automation adapter is the sole narrow exception; see `dev-docs/DEVELOPER_AUTOMATION.md`. +- There is no legacy `packages/opencode-plugin/`. Do not restore the V1 compatibility runtime or add general plugin extension points. The reviewed automation adapter shared by Developer Automation and native browser previews is the sole narrow exception; see `dev-docs/DEVELOPER_AUTOMATION.md` and `dev-docs/BROWSER_AUTOMATION.md`. - The server uses the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to one externally owned global OpenCode daemon. It owns no private port/database/registration/PID and never stops the daemon on backend shutdown. WSL requires Windows localhost forwarding and uses no cross-namespace PID operations. - The UI uses generated Promise clients from `OpenCode.make()` through the CodeNomad proxy. - OpenCode owns session APIs, native Forms, session Shell (`client.session.shell`), session instructions (`client.session.instructions.entry`), location-scoped background Shells, and interactive PTYs. Question request/reply/reject routes are compatibility-only; new interruption flows use `client.form.*`. The Status panel lists `client.shell.*` records, refreshes on Shell events/reconnect, displays native metadata, and supports ownership-checked removal. Interactive `client.pty.*` terminals remain separate. - CodeNomad owns explicit Stop Workspace eviction, directory authorization, Git status/diff/stage/unstage/commit, Yolo persistence/auto-replies, and `/api/events`. Tab/window close only detaches local UI and never evicts. - OpenCode owns the global daemon's standard state and database. Allowed configured environment variables apply only to `service start` for a missing daemon; an existing daemon is unchanged, and `OPENCODE_DB`/`XDG_STATE_HOME` ownership settings are ignored. - Native desktop identity is channel plus config profile: one singleton process/backend per profile, multiple UUID windows, second-launch focus by default, and `--new-window` for another window. Stable/dev/non-default profiles isolate native state; OpenCode sessions/messages are shared while tabs/drafts/views are per-window. -- Client-state V3 is a per-window envelope over the V2 content-addressed partition graph with atomic publication/migration, ownership-fenced writes, and conservative post-commit GC. Native SideCar/browser previews are sandboxed without same-origin access; DOM comment inspection is web-only. +- Client-state V3 is a per-window envelope over the V2 content-addressed partition graph with atomic publication/migration, ownership-fenced writes, and conservative post-commit GC. Electron and Windows Tauri browser previews use hardened native child webviews; iframe previews remain sandboxed without same-origin access and DOM comment inspection is web-only. ## Package Map @@ -47,7 +48,8 @@ description: | - Git mutations: `packages/server/src/workspaces/git-mutations.ts` - Yolo: `packages/server/src/permissions/`, `packages/server/src/server/routes/yolo.ts` - Desktop hosts: `packages/electron-app/electron/main/`, `packages/electron-app/electron/preload/index.cjs`, `packages/tauri-app/src-tauri/src/` -- Developer Automation: `packages/server/src/opencode/automation-plugin.ts`, `packages/server/src/developer-cdp.ts` +- Automation bridge: `packages/server/src/opencode/automation-plugin.ts`, `packages/server/src/server/routes/automation-plugin.ts` +- Native browser previews: `packages/electron-app/electron/main/browser-controller.ts`, `packages/tauri-app/src-tauri/src/browser_controller.rs`, `packages/ui/src/lib/native/browser.ts` ## Rules @@ -67,7 +69,7 @@ description: | | Public `@opencode-ai/sdk` examples | Installed experimental `@opencode-ai/client` declarations | | One `opencode serve` per workspace | One externally owned global daemon through the official CLI lifecycle | | Per-worktree clients/processes | Root proxy client plus native location/directory inputs | -| Reintroducing the V1 `packages/opencode-plugin` or general server plugin/background-process paths | Native OpenCode APIs; the reviewed Developer Automation adapter only for desktop feedback | +| Reintroducing the V1 `packages/opencode-plugin` or general server plugin/background-process paths | Native OpenCode APIs; the reviewed automation adapter only for desktop feedback and browser previews | | OpenCode APIs for stage/commit/Yolo policy | CodeNomad routes and managers | | Hardcoded UI strings | `t()` / `tGlobal()` and every locale | diff --git a/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md b/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md index 16b1feabc..d49241eb7 100644 --- a/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md +++ b/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md @@ -12,7 +12,7 @@ - Use `OpenCodeSharedService` in `packages/server/src/workspaces/opencode-service.ts`. - Keep one shared-service adapter and one event subscription for all workspaces. Use the selected host or WSL CLI's official status/start/password lifecycle, own no private service state/PID, and never stop the externally owned global daemon on backend shutdown. - Model workspaces with native `LocationRef`/directories in `packages/server/src/workspaces/manager.ts`. -- Never spawn or stop OpenCode per workspace and never add general plugin installation/packaging. Developer Automation's reviewed, execution-gated adapter is the sole exception; definitions are global until OpenCode supports dynamic location-scoped registration. +- Never spawn or stop OpenCode per workspace and never add general plugin installation/packaging. The reviewed, execution-gated adapter shared by Developer Automation and browser previews is the sole exception; definitions are global until OpenCode supports dynamic location-scoped registration. - Explicit Stop Workspace evicts the location; ordinary UI close never calls workspace deletion. WSL requires localhost forwarding and no cross-namespace PID operations. - Leave global service state/database ownership to OpenCode. Pass allowed environment only when starting a missing daemon; leave an existing daemon unchanged and ignore `OPENCODE_DB`/`XDG_STATE_HOME`. @@ -45,6 +45,6 @@ - CodeNomad SSE: `packages/server/src/server/routes/events.ts` - Git reads/mutations: `packages/server/src/workspaces/git-status.ts`, `git-mutations.ts` - Yolo: `packages/server/src/permissions/`, `packages/server/src/server/routes/yolo.ts` -- Developer Automation: `packages/server/src/opencode/automation-plugin.ts`, `packages/server/src/server/routes/automation-plugin.ts` +- Developer and browser automation: `packages/server/src/opencode/automation-plugin.ts`, `packages/server/src/server/routes/automation-plugin.ts` Deleted paths such as `packages/server/src/workspaces/runtime.ts`, `packages/server/src/background-processes/`, `packages/server/src/plugins/`, and `packages/opencode-plugin/` are not valid extension points. diff --git a/dev-docs/BROWSER_AUTOMATION.md b/dev-docs/BROWSER_AUTOMATION.md new file mode 100644 index 000000000..94f284144 --- /dev/null +++ b/dev-docs/BROWSER_AUTOMATION.md @@ -0,0 +1,30 @@ +# Browser Automation + +CodeNomad exposes `codenomad.browser` for controlling the visible web preview attached to the current OpenCode session. The supported actions are `open`, `navigate`, `snapshot`, `click`, `type`, and `screenshot`. + +## Shared Bridge + +Browser control extends the Developer Automation adapter instead of installing another plugin or listener: + +- `packages/server/src/opencode/automation-plugin.ts` installs the single `codenomad-automation.ts` OpenCode plugin and publishes one loopback registration. +- `packages/server/src/server/routes/automation-plugin.ts` serves the existing `/api/opencode-plugin/automation` endpoint with the existing per-process token. +- `browser-claim` verifies that exactly one CodeNomad instance owns the session before an `open` action asks the UI to create a preview. +- `browser-probe` also requires a visible native preview registered to that session. +- `browser-execute` validates the browser action and forwards `browser.probe` or `browser.execute` over the existing fenced native-parent request channel. + +The bridge remains loopback-only, token-authenticated, bounded, and session-location gated. The removed V1 plugin runtime and browser-specific bridge routes are not part of this design. + +## Native Hosts + +Electron uses a hardened `` with per-session storage. Attachment verifies guest ownership, allows only credential-free HTTP(S) URLs, denies permissions and downloads, and uses Chromium accessibility/CDP commands for snapshots and actions. + +Windows Tauri uses a child webview owned by the local application window. Commands, capabilities, navigation checks, bounds, visibility, and storage are managed by `browser_controller.rs`. Other Tauri platforms continue to use the existing iframe preview and do not advertise native browser automation. + +The UI routes an autonomous open request to the one loaded instance containing the session, claims the request for one local window, selects that session, and opens its preview. Hidden or duplicate targets are rejected rather than selected implicitly. + +## Focused Validation + +- Server: `automation-plugin.test.ts` and `routes/automation-plugin.test.ts` +- Electron: `browser-controller.test.ts`, `browser-webview-security.test.ts`, preload tests +- Tauri: `browser_controller.rs` tests plus command capability generation +- UI: `browser-frame-security.test.ts` and `lib/native/browser.test.ts` diff --git a/dev-docs/DEVELOPER_AUTOMATION.md b/dev-docs/DEVELOPER_AUTOMATION.md index 7c1e298c1..764c3b8eb 100644 --- a/dev-docs/DEVELOPER_AUTOMATION.md +++ b/dev-docs/DEVELOPER_AUTOMATION.md @@ -27,7 +27,7 @@ The small automation adapter registers these definitions with OpenCode: OpenCode plugin setup is currently one-shot, so definitions are visible in unrelated locations. Calls remain inert unless the bridge verifies that the current session location is owned by the active CodeNomad instance. -The adapter is intentionally narrower than the removed V1 plugin runtime. It does not own OpenCode lifecycle or state, spawn one daemon per workspace, or expose autonomous browser previews. +The adapter is intentionally narrower than the removed V1 plugin runtime. It does not own OpenCode lifecycle or state or spawn one daemon per workspace. Autonomous browser previews share its authenticated bridge and native transport but keep separate browser actions and host controllers; see [BROWSER_AUTOMATION.md](BROWSER_AUTOMATION.md). ## Trust Boundaries diff --git a/dev-docs/INDEX.md b/dev-docs/INDEX.md index 42e95b66e..c6340e428 100644 --- a/dev-docs/INDEX.md +++ b/dev-docs/INDEX.md @@ -28,6 +28,10 @@ Measured DEV-v2 growth analysis, maintainer position on test volume, ranked redu Isolated Electron/Tauri developer runs, CDP feedback tools, lifecycle guarantees, and trust boundaries. +### [BROWSER_AUTOMATION.md](BROWSER_AUTOMATION.md) + +Autonomous native browser previews, shared automation bridge routing, host security, and focused validation. + --- ## Specification Documents diff --git a/dev-docs/architecture.md b/dev-docs/architecture.md index d33835817..3ae5f88b1 100644 --- a/dev-docs/architecture.md +++ b/dev-docs/architecture.md @@ -34,7 +34,7 @@ Electron and Tauri run one native singleton process and one CodeNomad backend pe OpenCode sessions and messages remain shared through the global daemon. Window membership, tabs, drafts, view state, and native bounds are local to each UUID window. Client-state V3 is a per-window envelope over the V2 content-addressed partition graph: immutable partitions are prepared before atomic root publication, writes and migrations are fenced by current ownership, and garbage collection runs after publication while retaining every partition referenced by any window. -Previews use unguessable capabilities for HTTP and WebSocket traffic. Native previews route a token-scoped `.preview.localhost` origin to the pinned target; web clients use the equivalent path route. SideCar/browser frames remain opaque-origin sandboxes without `allow-same-origin`; preview element comments use a source-checked message bridge instead of parent DOM access. +Previews use unguessable capabilities for HTTP and WebSocket traffic. Electron and Windows Tauri local windows open HTTP(S) pages in hardened native child webviews with isolated storage; other clients use the existing capability-scoped iframe proxy. SideCar/browser iframes remain opaque-origin sandboxes without `allow-same-origin`; preview element comments use a source-checked message bridge instead of parent DOM access. ## API Boundaries @@ -72,8 +72,9 @@ Current native events include session lifecycle/output events (`session.created` | Yolo state, persistence and auto-accept | CodeNomad server | | Browser SSE multiplexing | CodeNomad server | | Developer Automation launch and CDP feedback | CodeNomad desktop hosts and authenticated automation adapter | +| Autonomous browser previews | CodeNomad desktop browser controllers and the same authenticated automation adapter | -Session Shell remains separate from background Shell and PTY management. The Status panel lists location-scoped native background Shells, refreshes on Shell events/reconnect, displays native metadata, and allows ownership-checked removal. Output requests preserve native cursor pagination. Interactive PTYs remain separate. `packages/opencode-plugin` and the server plugin/background-process paths remain deleted and must not be restored; Developer Automation is the only reviewed adapter exception. +Session Shell remains separate from background Shell and PTY management. The Status panel lists location-scoped native background Shells, refreshes on Shell events/reconnect, displays native metadata, and allows ownership-checked removal. Output requests preserve native cursor pagination. Interactive PTYs remain separate. `packages/opencode-plugin` and the server plugin/background-process paths remain deleted and must not be restored; the narrow automation adapter used by Developer Automation and browser previews is the only reviewed exception. ## Persistence diff --git a/packages/electron-app/electron/main/browser-controller.test.ts b/packages/electron-app/electron/main/browser-controller.test.ts new file mode 100644 index 000000000..ebe79f103 --- /dev/null +++ b/packages/electron-app/electron/main/browser-controller.test.ts @@ -0,0 +1,282 @@ +import assert from "node:assert/strict" +import { EventEmitter } from "node:events" +import { describe, it } from "node:test" +import type { BrowserWindow, WebContents } from "electron" +import { BrowserController } from "./browser-controller" + +interface GuestOptions { + id: number + owner: WebContents + loadURL?: (url: string) => Promise + sendCommand?: (method: string, params?: Record) => Promise + loading?: boolean +} + +function createGuest(options: GuestOptions) { + const events = new EventEmitter() + let attached = false + let url = "https://example.com/" + let attachCount = 0 + let detachCount = 0 + const guest = Object.assign(events, { + id: options.id, + hostWebContents: options.owner, + getType: () => "webview", + isDestroyed: () => false, + isLoading: () => options.loading ?? false, + stop: () => undefined, + getURL: () => url, + loadURL: async (nextUrl: string) => { + await options.loadURL?.(nextUrl) + url = nextUrl + }, + debugger: { + isAttached: () => attached, + attach: () => { attached = true; attachCount += 1 }, + detach: () => { attached = false; detachCount += 1 }, + sendCommand: options.sendCommand ?? (async () => ({})), + }, + }) as unknown as WebContents + return { guest, events, counts: () => ({ attachCount, detachCount }) } +} + +function createHarness(requestOpen: (sessionID: string, url: string, requestID: string) => void = () => {}) { + const owner = { id: 1, isDestroyed: () => false } as WebContents + const guests = new Map() + const window = { isDestroyed: () => false, isVisible: () => true, isMinimized: () => false } as BrowserWindow + const controller = new BrowserController(requestOpen, { + fromId: (id) => guests.get(id) ?? null, + fromWebContents: () => window, + }) + const add = (guest: WebContents, registrationId: string, sessionId = "session") => { + guests.set(guest.id, guest) + controller.observeGuest(owner, guest) + controller.register(owner, { sessionId, registrationId, guestWebContentsId: guest.id }) + } + return { controller, owner, add } +} + +describe("BrowserController", () => { + it("serializes debugger commands for each guest", async () => { + const harness = createHarness() + let releaseTree!: () => void + const treeGate = new Promise((resolve) => { releaseTree = resolve }) + let treeCalls = 0 + const { guest, counts } = createGuest({ + id: 10, + owner: harness.owner, + sendCommand: async (method) => { + if (method !== "Accessibility.getFullAXTree") return {} + treeCalls += 1 + if (treeCalls === 1) await treeGate + return { nodes: [] } + }, + }) + harness.add(guest, "one") + + const first = harness.controller.execute("session", { action: "snapshot" }) + while (treeCalls === 0) await Promise.resolve() + const second = harness.controller.execute("session", { action: "snapshot" }) + await Promise.resolve() + assert.equal(treeCalls, 1) + + releaseTree() + await Promise.all([first, second]) + assert.equal(treeCalls, 2) + assert.deepEqual(counts(), { attachCount: 2, detachCount: 2 }) + }) + + it("rejects queued commands after their session target unregisters", async () => { + const harness = createHarness() + let releaseTree!: () => void + const treeGate = new Promise((resolve) => { releaseTree = resolve }) + let treeCalls = 0 + const { guest } = createGuest({ + id: 13, + owner: harness.owner, + sendCommand: async (method) => { + if (method !== "Accessibility.getFullAXTree") return {} + treeCalls += 1 + if (treeCalls === 1) await treeGate + return { nodes: [] } + }, + }) + harness.add(guest, "one") + + const first = harness.controller.execute("session", { action: "snapshot" }) + while (treeCalls === 0) await Promise.resolve() + const queued = harness.controller.execute("session", { action: "snapshot" }) + harness.controller.unregister(harness.owner, "one") + releaseTree() + + await first + await assert.rejects(queued, /No visible local browser target/) + assert.equal(treeCalls, 1) + }) + + it("allows only one window to claim a missing-target open", async () => { + let requestID = "" + const harness = createHarness((_sessionID, _url, id) => { requestID = id }) + const pending = harness.controller.execute("session", { action: "open", url: "https://example.com/next" }) + assert.ok(requestID) + assert.equal(harness.controller.claimOpen(harness.owner, requestID), true) + const otherOwner = { id: 2, isDestroyed: () => false } as WebContents + assert.equal(harness.controller.claimOpen(otherOwner, requestID), false) + + const { guest, events } = createGuest({ id: 14, owner: harness.owner, loading: true }) + harness.add(guest, "one") + events.emit("did-start-navigation", {}, "https://example.com/next", false, true) + events.emit("did-finish-load") + await pending + assert.equal(harness.controller.claimOpen(harness.owner, requestID), false) + }) + + it("does not publish snapshot refs when a frame navigates during capture", async () => { + const harness = createHarness() + let releaseTree!: () => void + const treeGate = new Promise((resolve) => { releaseTree = resolve }) + let treeStarted = false + const { guest, events } = createGuest({ + id: 11, + owner: harness.owner, + sendCommand: async (method) => { + if (method !== "Accessibility.getFullAXTree") return {} + treeStarted = true + await treeGate + return { nodes: [{ backendDOMNodeId: 42, role: { value: "button" }, name: { value: "Save" } }] } + }, + }) + harness.add(guest, "one") + const snapshot = harness.controller.execute("session", { action: "snapshot" }) + while (!treeStarted) await Promise.resolve() + + events.emit("did-frame-navigate") + releaseTree() + await snapshot + + await assert.rejects(harness.controller.execute("session", { action: "click", ref: "e1" }), /take a new snapshot/) + }) + + it("reports ambiguity while waiting for a renderer target", async () => { + let controller!: BrowserController + const owner = { id: 1, isDestroyed: () => false } as WebContents + const guests = new Map() + const window = { isDestroyed: () => false, isVisible: () => true, isMinimized: () => false } as BrowserWindow + controller = new BrowserController(() => { + for (const id of [20, 21]) { + const { guest } = createGuest({ id, owner }) + guests.set(id, guest) + controller.observeGuest(owner, guest) + controller.register(owner, { sessionId: "session", registrationId: `registration-${id}`, guestWebContentsId: id }) + } + }, { + fromId: (id) => guests.get(id) ?? null, + fromWebContents: () => window, + }) + + await assert.rejects( + controller.execute("session", { action: "open", url: "https://example.com/" }), + /Multiple visible browser targets exist for this session/, + ) + }) + + it("propagates the initial renderer load failure after opening a missing target", async () => { + let harness!: ReturnType + harness = createHarness(() => { + const { guest, events } = createGuest({ id: 22, owner: harness.owner, loading: true }) + harness.add(guest, "one") + setTimeout(() => events.emit("did-fail-load", {}, -102, "connection refused", "http://127.0.0.1:43127/", true), 75) + }) + + await assert.rejects( + harness.controller.execute("session", { action: "open", url: "http://127.0.0.1:43127" }), + /connection refused \(-102\)/, + ) + }) + + it("propagates an initial load failure that occurs before registration", async () => { + let harness!: ReturnType + harness = createHarness(() => { + const { guest, events } = createGuest({ id: 23, owner: harness.owner, loading: false }) + harness.add(guest, "one") + events.emit("did-fail-load", {}, -102, "connection refused", "http://127.0.0.1:43128/", true) + }) + + await assert.rejects( + harness.controller.execute("session", { action: "open", url: "http://127.0.0.1:43128" }), + /connection refused \(-102\)/, + ) + }) + + it("does not satisfy open with a completed load for another URL", async () => { + let harness!: ReturnType + harness = createHarness(() => { + const { guest, events } = createGuest({ id: 24, owner: harness.owner, loading: true }) + harness.add(guest, "one") + events.emit("did-finish-load") + setTimeout(() => events.emit("did-fail-load", {}, -102, "connection refused", "http://127.0.0.1:43128/", true), 25) + }) + + await assert.rejects( + harness.controller.execute("session", { action: "open", url: "http://127.0.0.1:43128" }), + /connection refused \(-102\)/, + ) + }) + + it("ignores a live load completion for another URL", async () => { + let harness!: ReturnType + harness = createHarness(() => { + const { guest, events } = createGuest({ id: 25, owner: harness.owner, loading: true }) + harness.add(guest, "one") + setTimeout(() => events.emit("did-finish-load"), 25) + setTimeout(() => events.emit("did-fail-load", {}, -102, "connection refused", "http://127.0.0.1:43128/", true), 50) + }) + + await assert.rejects( + harness.controller.execute("session", { action: "open", url: "http://127.0.0.1:43128" }), + /connection refused \(-102\)/, + ) + }) + + it("propagates load failures and continues the guest queue", async () => { + const harness = createHarness() + const { guest } = createGuest({ + id: 12, + owner: harness.owner, + loadURL: async () => { throw new Error("connection refused") }, + sendCommand: async (method) => method === "Accessibility.getFullAXTree" ? { nodes: [] } : {}, + }) + harness.add(guest, "one") + + await assert.rejects(harness.controller.execute("session", { action: "navigate", url: "https://example.com/fail" }), /connection refused/) + assert.deepEqual(await harness.controller.execute("session", { action: "snapshot" }), { + url: "https://example.com/", + snapshot: "No accessible elements found", + }) + }) + + it("expires stalled debugger work without blocking the guest queue", async () => { + const harness = createHarness() + let calls = 0 + const { guest } = createGuest({ + id: 26, + owner: harness.owner, + sendCommand: async (method) => { + if (method !== "Accessibility.getFullAXTree") return {} + calls += 1 + if (calls === 1) return new Promise(() => {}) + return { nodes: [] } + }, + }) + harness.add(guest, "one") + + await assert.rejects( + harness.controller.execute("session", { action: "snapshot" }, Date.now() + 20), + /timed out/, + ) + assert.deepEqual(await harness.controller.execute("session", { action: "snapshot" }), { + url: "https://example.com/", + snapshot: "No accessible elements found", + }) + }) +}) diff --git a/packages/electron-app/electron/main/browser-controller.ts b/packages/electron-app/electron/main/browser-controller.ts new file mode 100644 index 000000000..028824f39 --- /dev/null +++ b/packages/electron-app/electron/main/browser-controller.ts @@ -0,0 +1,393 @@ +import { createRequire } from "node:module" +import { randomUUID } from "node:crypto" +import type { BrowserWindow, WebContents } from "electron" +import { isBrowserUrlAllowed } from "./browser-webview-security" + +const electron = createRequire(import.meta.url)("electron") as typeof import("electron") + +interface BrowserControllerElectron { + fromId(id: number): WebContents | undefined | null + fromWebContents(contents: WebContents): BrowserWindow | null +} + +interface RegistrationInput { + sessionId: string + registrationId: string + guestWebContentsId: number +} + +interface Registration extends RegistrationInput { + owner: WebContents + guest: WebContents +} + +type BrowserCommand = + | { action: "open"; url: string } + | { action: "navigate"; url: string } + | { action: "snapshot" } + | { action: "click"; ref: string } + | { action: "type"; ref: string; text: string; clear?: boolean } + | { action: "screenshot" } + +interface AxNode { + ignored?: boolean + backendDOMNodeId?: number + role?: { value?: unknown } + name?: { value?: unknown } + value?: { value?: unknown } + properties?: Array<{ name?: string; value?: { value?: unknown } }> +} + +export class BrowserController { + private readonly registrations = new Map() + private readonly observedGuests = new Map() + private readonly refs = new Map>() + private readonly commandQueues = new WeakMap>() + private readonly navigationVersions = new WeakMap() + private readonly loadResults = new WeakMap() + private readonly openClaims = new Map() + + constructor( + private readonly requestOpen: (sessionID: string, url: string, requestID: string) => void, + private readonly electronApi: BrowserControllerElectron = { + fromId: (id) => electron.webContents.fromId(id), + fromWebContents: (contents) => electron.BrowserWindow.fromWebContents(contents), + }, + ) {} + + observeGuest(owner: WebContents, guest: WebContents): void { + this.observedGuests.set(guest.id, owner.id) + this.navigationVersions.set(guest, 0) + let startedUrl = guest.getURL() + const clearRefs = () => { + this.refs.delete(guest.id) + this.navigationVersions.set(guest, (this.navigationVersions.get(guest) ?? 0) + 1) + } + guest.on("did-navigate", clearRefs) + guest.on("did-navigate-in-page", clearRefs) + guest.on("did-frame-navigate", clearRefs) + guest.on("did-start-navigation", (_event, url, _isInPlace, isMainFrame) => { + if (isMainFrame) startedUrl = url + }) + guest.on("did-finish-load", () => this.loadResults.set(guest, { at: Date.now(), url: guest.getURL(), startedUrl })) + guest.on("did-fail-load", (_event, code, description, url, isMainFrame) => { + if (isMainFrame && code !== -3) this.loadResults.set(guest, { + at: Date.now(), + url, + startedUrl, + error: new Error(`${description} (${code}) loading ${url}`), + }) + }) + guest.once("destroyed", () => { + this.observedGuests.delete(guest.id) + this.refs.delete(guest.id) + for (const [id, registration] of this.registrations) { + if (registration.guest.id === guest.id) this.registrations.delete(id) + } + }) + } + + register(owner: WebContents, input: RegistrationInput): void { + if (!validId(input.sessionId, 256) || !validId(input.registrationId, 128) || !Number.isInteger(input.guestWebContentsId)) { + throw new Error("Invalid browser target registration") + } + const guest = this.electronApi.fromId(input.guestWebContentsId) + if (!guest || guest.isDestroyed() || guest.getType() !== "webview" + || guest.hostWebContents !== owner || this.observedGuests.get(guest.id) !== owner.id) { + throw new Error("Browser guest does not belong to this window") + } + this.registrations.set(input.registrationId, { ...input, owner, guest }) + } + + unregister(owner: WebContents, registrationId: unknown): void { + if (typeof registrationId !== "string") return + const registration = this.registrations.get(registrationId) + if (registration?.owner === owner) this.registrations.delete(registrationId) + } + + claimOpen(owner: WebContents, requestID: unknown): boolean { + if (typeof requestID !== "string" || !this.openClaims.has(requestID) || owner.isDestroyed()) return false + const claimed = this.openClaims.get(requestID) + if (claimed && claimed !== owner) return false + this.openClaims.set(requestID, owner) + return true + } + + removeOwner(owner: WebContents): void { + for (const [id, registration] of this.registrations) { + if (registration.owner === owner) this.registrations.delete(id) + } + } + + probe(sessionID: string): { available: boolean } { + this.resolve(sessionID) + return { available: true } + } + + async execute(sessionID: string, command: BrowserCommand, deadline = Number.POSITIVE_INFINITY): Promise { + if (command.action === "open") return this.open(sessionID, command.url, deadline) + const { guest } = this.resolve(sessionID) + return this.enqueue(guest, () => { + ensureDeadline(deadline) + this.assertCurrentTarget(sessionID, guest) + return this.executeGuest(guest, command, deadline) + }) + } + + private async executeGuest(guest: WebContents, command: Exclude, deadline: number): Promise { + switch (command.action) { + case "navigate": { + if (!isBrowserUrlAllowed(command.url)) throw new Error("Browser navigation requires a credential-free HTTP(S) URL") + this.refs.delete(guest.id) + await withDeadline(() => guest.loadURL(command.url), deadline, () => guest.stop()) + return { url: guest.getURL() } + } + case "snapshot": + return this.snapshot(guest, deadline) + case "click": + await this.click(guest, command.ref, deadline) + return { clicked: command.ref, url: guest.getURL() } + case "type": + await this.click(guest, command.ref, deadline) + if (command.clear !== false) await clearFocusedField(guest, deadline) + await withDebugger(guest, deadline, (debuggerSession) => debuggerSession.sendCommand("Input.insertText", { text: command.text })) + return { typed: command.ref, url: guest.getURL() } + case "screenshot": { + const result = await withDebugger(guest, deadline, (debuggerSession) => debuggerSession.sendCommand("Page.captureScreenshot", { + format: "png", + fromSurface: true, + })) as { data?: unknown } + if (typeof result.data !== "string" || result.data.length > 16 * 1024 * 1024) throw new Error("Browser screenshot is unavailable or too large") + return { image: { mime: "image/png", data: result.data }, url: guest.getURL() } + } + } + } + + private async open(sessionID: string, url: string, deadline: number): Promise<{ url: string }> { + if (!isBrowserUrlAllowed(url)) throw new Error("Browser navigation requires a credential-free HTTP(S) URL") + try { + const { guest } = this.resolve(sessionID) + return this.enqueue(guest, async () => { + ensureDeadline(deadline) + this.assertCurrentTarget(sessionID, guest) + this.refs.delete(guest.id) + await withDeadline(() => guest.loadURL(url), deadline, () => guest.stop()) + this.assertCurrentTarget(sessionID, guest) + return { url: guest.getURL() } + }) + } catch (error) { + if (!(error instanceof Error) || error.message !== "No visible local browser target for this session") throw error + } + + const requestID = randomUUID() + this.openClaims.set(requestID, undefined) + return new Promise<{ url: string }>((resolve, reject) => { + const requestedAt = Date.now() + const timeout = setTimeout(() => { + clearInterval(interval) + reject(new Error("Timed out waiting for CodeNomad to open the browser preview")) + }, 10_000) + const interval = setInterval(() => { + try { + ensureDeadline(deadline) + const { guest } = this.resolve(sessionID) + clearInterval(interval) + clearTimeout(timeout) + void this.enqueue(guest, async () => { + ensureDeadline(deadline) + this.assertCurrentTarget(sessionID, guest) + await waitForMainFrameLoad(guest, url, requestedAt, deadline, () => this.loadResults.get(guest)) + this.assertCurrentTarget(sessionID, guest) + return { url: guest.getURL() } + }).then( + resolve, + reject, + ) + } catch (error) { + if (error instanceof Error && error.message === "No visible local browser target for this session") return + clearInterval(interval) + clearTimeout(timeout) + reject(error) + } + }, 50) + this.requestOpen(sessionID, url, requestID) + }).finally(() => this.openClaims.delete(requestID)) + } + + private resolve(sessionID: string): Registration { + const matches = [...this.registrations.values()].filter((registration) => { + if (registration.sessionId !== sessionID || registration.owner.isDestroyed() || registration.guest.isDestroyed()) return false + const window = this.electronApi.fromWebContents(registration.owner) + return Boolean(window && !window.isDestroyed() && window.isVisible() && !window.isMinimized()) + }) + if (matches.length === 0) throw new Error("No visible local browser target for this session") + if (matches.length > 1) throw new Error("Multiple visible browser targets exist for this session") + return matches[0] + } + + private assertCurrentTarget(sessionID: string, guest: WebContents): void { + if (this.resolve(sessionID).guest !== guest) throw new Error("Browser target changed before command execution") + } + + private async snapshot(guest: WebContents, deadline: number): Promise<{ url: string; snapshot: string }> { + const navigationVersion = this.navigationVersions.get(guest) + const result = await withDebugger(guest, deadline, async (debuggerSession) => { + await debuggerSession.sendCommand("Accessibility.enable") + return debuggerSession.sendCommand("Accessibility.getFullAXTree") + }) as { nodes?: AxNode[] } + const refs = new Map() + const lines: string[] = [] + for (const node of result.nodes ?? []) { + if (node.ignored || !node.backendDOMNodeId) continue + const role = stringValue(node.role?.value) + const name = stringValue(node.name?.value) + if (!role || (!name && ["generic", "none", "StaticText", "InlineTextBox"].includes(role))) continue + const ref = `e${refs.size + 1}` + refs.set(ref, node.backendDOMNodeId) + const value = stringValue(node.value?.value) + const states = (node.properties ?? []) + .filter((property) => ["checked", "disabled", "expanded", "focused", "selected"].includes(property.name ?? "")) + .map((property) => `${property.name}=${String(property.value?.value)}`) + lines.push(`[${ref}] ${role}${name ? ` ${JSON.stringify(name)}` : ""}${value ? ` value=${JSON.stringify(value)}` : ""}${states.length ? ` ${states.join(" ")}` : ""}`) + if (lines.length >= 750 || lines.join("\n").length >= 64 * 1024) break + } + if (this.navigationVersions.get(guest) === navigationVersion) this.refs.set(guest.id, refs) + return { url: guest.getURL(), snapshot: lines.join("\n") || "No accessible elements found" } + } + + private async click(guest: WebContents, ref: string, deadline: number): Promise { + const backendNodeId = this.refs.get(guest.id)?.get(ref) + if (!backendNodeId) throw new Error(`Unknown browser ref ${ref}; take a new snapshot`) + await withDebugger(guest, deadline, async (debuggerSession) => { + await debuggerSession.sendCommand("DOM.scrollIntoViewIfNeeded", { backendNodeId }) + const result = await debuggerSession.sendCommand("DOM.getBoxModel", { backendNodeId }) as { model?: { content?: number[] } } + const box = result.model?.content + if (!box || box.length !== 8) throw new Error(`Browser ref ${ref} is not visible`) + const x = (box[0] + box[2] + box[4] + box[6]) / 4 + const y = (box[1] + box[3] + box[5] + box[7]) / 4 + await debuggerSession.sendCommand("Input.dispatchMouseEvent", { type: "mouseMoved", x, y }) + await debuggerSession.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 }) + await debuggerSession.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 }) + }) + } + + private enqueue(guest: WebContents, operation: () => Promise): Promise { + const result = (this.commandQueues.get(guest) ?? Promise.resolve()).then(operation, operation) + const tail = result.then(() => undefined, () => undefined) + this.commandQueues.set(guest, tail) + void tail.then(() => { + if (this.commandQueues.get(guest) === tail) this.commandQueues.delete(guest) + }) + return result + } +} + +export async function handleNativeBrowserRequest(controller: BrowserController, method: string, params: unknown, deadline = Number.POSITIVE_INFINITY): Promise { + const input = params as { sessionID?: unknown; command?: unknown } | undefined + if (!input || typeof input.sessionID !== "string") throw new Error("Invalid browser request") + if (method === "browser.probe") return controller.probe(input.sessionID) + if (method === "browser.execute" && input.command && typeof input.command === "object") { + return controller.execute(input.sessionID, input.command as BrowserCommand, deadline) + } + throw new Error(`Unsupported native method: ${method}`) +} + +function ensureDeadline(deadline: number): void { + if (Date.now() >= deadline) throw new Error("Native request expired before execution") +} + +function validId(value: unknown, max: number): value is string { + return typeof value === "string" && value.length > 0 && value.length <= max +} + +function stringValue(value: unknown): string { + return typeof value === "string" ? value.replace(/\s+/g, " ").trim().slice(0, 500) : "" +} + +async function withDebugger(guest: WebContents, deadline: number, operation: (debuggerSession: WebContents["debugger"]) => Promise): Promise { + const attached = guest.debugger.isAttached() + if (!attached) guest.debugger.attach("1.3") + try { + return await withDeadline(() => operation(guest.debugger), deadline, () => { + if (guest.debugger.isAttached()) guest.debugger.detach() + }) + } finally { + if (!attached && guest.debugger.isAttached()) guest.debugger.detach() + } +} + +async function clearFocusedField(guest: WebContents, deadline: number): Promise { + await withDebugger(guest, deadline, async (debuggerSession) => { + const mac = process.platform === "darwin" + const modifier = mac ? { key: "Meta", code: "MetaLeft", modifiers: 4 } : { key: "Control", code: "ControlLeft", modifiers: 2 } + await debuggerSession.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", ...modifier }) + await debuggerSession.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", key: "a", code: "KeyA", modifiers: modifier.modifiers }) + await debuggerSession.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", key: "a", code: "KeyA", modifiers: modifier.modifiers }) + await debuggerSession.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", key: modifier.key, code: modifier.code }) + }) +} + +async function withDeadline(operation: () => Promise, deadline: number, onTimeout?: () => void): Promise { + if (!Number.isFinite(deadline)) return operation() + ensureDeadline(deadline) + let timeout: NodeJS.Timeout | undefined + const expired = new Promise((_resolve, reject) => { + timeout = setTimeout(() => { + try { + onTimeout?.() + } finally { + reject(new Error("Native browser command timed out")) + } + }, Math.max(1, deadline - Date.now())) + }) + try { + return await Promise.race([operation(), expired]) + } finally { + if (timeout) clearTimeout(timeout) + } +} + +async function waitForMainFrameLoad( + guest: WebContents, + expectedUrl: string, + requestedAt: number, + deadline: number, + loadResult: () => { at: number; url: string; startedUrl: string; error?: Error } | undefined, +): Promise { + const expected = new URL(expectedUrl).href + const completed = loadResult() + if (completed && completed.at >= requestedAt && matchesLoad(completed, expected)) { + if (completed.error) throw completed.error + return + } + if (!guest.isLoading() && guest.getURL() === expected) return + await new Promise((resolve, reject) => { + const cleanup = () => { + clearTimeout(timeout) + guest.removeListener("did-finish-load", finish) + guest.removeListener("did-fail-load", fail) + } + const finish = () => { + const result = loadResult() + if (!result || !matchesLoad(result, expected)) return + cleanup() + if (result.error) reject(result.error) + else resolve() + } + const fail = (_event: Electron.Event, code: number, _description: string, _url: string, isMainFrame: boolean) => { + if (!isMainFrame || code === -3) return + finish() + } + const timeoutMs = Math.min(10_000, Math.max(1, deadline - Date.now())) + const timeout = setTimeout(() => { + cleanup() + reject(new Error(Date.now() >= deadline ? "Native request expired before execution" : "Timed out waiting for the browser page to load")) + }, timeoutMs) + guest.once("did-finish-load", finish) + guest.on("did-fail-load", fail) + }) +} + +function matchesLoad(result: { url: string; startedUrl: string }, expected: string): boolean { + return new URL(result.url).href === expected || new URL(result.startedUrl).href === expected +} diff --git a/packages/electron-app/electron/main/browser-webview-security.test.ts b/packages/electron-app/electron/main/browser-webview-security.test.ts new file mode 100644 index 000000000..b973be4d3 --- /dev/null +++ b/packages/electron-app/electron/main/browser-webview-security.test.ts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import type { WebPreferences } from "electron" +import { browserPartition, isBrowserUrlAllowed, secureBrowserWebview } from "./browser-webview-security" + +describe("browser webview security", () => { + it("accepts only credential-free HTTP(S) URLs in the browser partition", () => { + assert.equal(isBrowserUrlAllowed("https://github.com/"), true) + assert.equal(isBrowserUrlAllowed("file:///tmp/index.html"), false) + assert.equal(isBrowserUrlAllowed("https://user:pass@example.com/"), false) + assert.equal(isBrowserUrlAllowed("http://localhost:3000/"), true) + assert.equal(isBrowserUrlAllowed("http://127.0.0.1:3000/"), true) + + const preferences = { preload: "malicious.js", nodeIntegration: true } as WebPreferences + assert.equal(secureBrowserWebview(preferences, { partition: browserPartition("session_1"), src: "https://github.com/" }), true) + assert.equal(preferences.preload, undefined) + assert.equal(preferences.nodeIntegration, false) + assert.equal(preferences.contextIsolation, true) + assert.equal(preferences.sandbox, true) + assert.equal(secureBrowserWebview({}, { partition: "persist:other", src: "https://github.com/" }), false) + assert.equal(secureBrowserWebview({}, { partition: "persist:codenomad-browser-../other", src: "https://github.com/" }), false) + assert.notEqual(browserPartition("session_1"), browserPartition("session_2")) + }) +}) diff --git a/packages/electron-app/electron/main/browser-webview-security.ts b/packages/electron-app/electron/main/browser-webview-security.ts new file mode 100644 index 000000000..8830df791 --- /dev/null +++ b/packages/electron-app/electron/main/browser-webview-security.ts @@ -0,0 +1,30 @@ +import type { WebPreferences } from "electron" + +export const browserPartitionPrefix = "persist:codenomad-browser-" + +export function browserPartition(sessionId: string): string { + if (!/^[A-Za-z0-9_-]{1,256}$/.test(sessionId)) throw new Error("Invalid browser session ID") + return `${browserPartitionPrefix}${sessionId}` +} + +export function isBrowserUrlAllowed(rawUrl: string): boolean { + try { + const url = new URL(rawUrl) + return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password + } catch { + return false + } +} + +export function secureBrowserWebview(webPreferences: WebPreferences, params: Record): boolean { + if (!params.partition.startsWith(browserPartitionPrefix) + || !/^[A-Za-z0-9_-]{1,256}$/.test(params.partition.slice(browserPartitionPrefix.length)) + || !isBrowserUrlAllowed(params.src)) return false + delete webPreferences.preload + webPreferences.nodeIntegration = false + webPreferences.contextIsolation = true + webPreferences.sandbox = true + webPreferences.webSecurity = true + webPreferences.allowRunningInsecureContent = false + return true +} diff --git a/packages/electron-app/electron/main/ipc.ts b/packages/electron-app/electron/main/ipc.ts index 81d01eff9..b95f31381 100644 --- a/packages/electron-app/electron/main/ipc.ts +++ b/packages/electron-app/electron/main/ipc.ts @@ -6,6 +6,7 @@ import { openWorkspaceTarget, type WorkspaceEditor, type WorkspaceOpenTarget } f import { setWorkspaceMenuEnabled } from "./menu" import { requireHttpUrl } from "./navigation-security" import type { DeveloperRunManager, DeveloperRunTarget } from "./developer-run-manager" +import type { BrowserController } from "./browser-controller" import path from "node:path" interface LocalSender { @@ -21,6 +22,7 @@ interface CliIPCDependencies { nextFolder(windowId: string): string | null acknowledgeFolder(windowId: string, folder: string, opened: boolean): void developerRunManager: DeveloperRunManager + browserController: BrowserController } interface DialogOpenRequest { @@ -120,6 +122,30 @@ export function setupCliIPC(cliManager: CliProcessManager, dependencies: CliIPCD local(event) await dependencies.developerRunManager.stop() }) + ipcMain.handle("browser-target:register", async (event, payload: { sessionId?: unknown; registrationId?: unknown; guestWebContentsId?: unknown }) => { + const { window } = local(event) + dependencies.browserController.register(window.webContents, payload as { + sessionId: string + registrationId: string + guestWebContentsId: number + }) + return { ok: true } + }) + ipcMain.handle("browser-target:claimOpen", async (event, requestID: unknown) => { + const window = local(event).window + const claimed = dependencies.browserController.claimOpen(window.webContents, requestID) + if (claimed) { + if (window.isMinimized()) window.restore() + window.show() + window.focus() + } + return claimed + }) + ipcMain.handle("browser-target:unregister", async (event, registrationId: unknown) => { + const { window } = local(event) + dependencies.browserController.unregister(window.webContents, registrationId) + return { ok: true } + }) ipcMain.handle("dialog:open", async (event, request: DialogOpenRequest): Promise => { const { window } = local(event) diff --git a/packages/electron-app/electron/main/main.ts b/packages/electron-app/electron/main/main.ts index b1de3ce40..e285e2c19 100644 --- a/packages/electron-app/electron/main/main.ts +++ b/packages/electron-app/electron/main/main.ts @@ -6,6 +6,8 @@ import { existsSync, mkdirSync, rmSync } from "node:fs" import { dirname, join } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import { DeveloperRunManager, handleNativeDeveloperRunRequest } from "./developer-run-manager" +import { BrowserController, handleNativeBrowserRequest } from "./browser-controller" +import { isBrowserUrlAllowed, secureBrowserWebview } from "./browser-webview-security" import { ClientStateManager } from "./client-state" import { setupClientStateIPC } from "./client-state-ipc" import { ClientStateNavigationController } from "./client-state-navigation" @@ -15,7 +17,7 @@ import { clearWorkspaceMenuWindow, createApplicationMenu, setWorkspaceMenuEnable import { resolveFocusedLocalTarget, resolveWindowTarget } from "./menu-target" import { MultiwindowLifecycle } from "./multiwindow-lifecycle" import { decideNavigation, requireHttpUrl } from "./navigation-security" -import { configureMediaPermissionHandlers, isAllowedRendererOrigin } from "./permissions" +import { configureBrowserPermissionHandlers, configureMediaPermissionHandlers, isAllowedRendererOrigin } from "./permissions" import { CliProcessManager } from "./process-manager" import { navigateRemoteWindow, RemoteWindowRegistry } from "./remote-window-registry" import { resolveConfiguredRendererOrigins } from "./renderer-origin" @@ -66,10 +68,18 @@ function runPrimary(firstIntent: LaunchIntent) { : undefined) const registry = new LocalWindowRegistry(async (id) => { await clientState.setActiveWindow(id) }) const developerRunManager = new DeveloperRunManager() - const cli = new CliProcessManager((method) => handleNativeDeveloperRunRequest(developerRunManager, method)) + const browserController = new BrowserController((sessionID, url, requestID) => { + for (const record of registry.all()) { + if (!record.window.isDestroyed()) record.window.webContents.send("browser-target:open", { sessionID, url, requestID }) + } + }) + const cli = new CliProcessManager((method, params, deadline) => method.startsWith("browser.") + ? handleNativeBrowserRequest(browserController, method, params, deadline) + : handleNativeDeveloperRunRequest(developerRunManager, method)) const remoteOrigins = new Map>() const insecureOrigins = new Map>() const navigationLifecycle = new SerializedLifecycle() + const securedBrowserSessions = new WeakSet() let backendUrl: string | null = null let backendTargetUrl: string | null = null const remoteWindows = new RemoteWindowRegistry((sessionId) => { @@ -160,7 +170,7 @@ function runPrimary(firstIntent: LaunchIntent) { ...(bounds ? { x: bounds.x, y: bounds.y } : {}), useContentSize: true, minWidth: 800, minHeight: 600, backgroundColor: "#1a1a1a", icon: getIconPath(), webPreferences: { - preload: getPreloadPath(), contextIsolation: true, nodeIntegration: false, spellcheck: !isMac, + preload: getPreloadPath(), contextIsolation: true, nodeIntegration: false, spellcheck: !isMac, webviewTag: true, ...(saved ? { zoomFactor: saved.zoomFactor } : {}), additionalArguments: ["--codenomad-window-context=local", `--codenomad-window-id=${windowId}`], }, @@ -179,6 +189,34 @@ function runPrimary(firstIntent: LaunchIntent) { lifecycle.attach(record) installWindowZoomInput(window, (level) => tracker ? tracker.setZoomLevel(level) : window.webContents.setZoomLevel(level)) setupNavigationGuards(window, navigation, getAllowedOrigins, getLoadingUrl) + window.webContents.on("will-attach-webview", (event, webPreferences, params) => { + if (!secureBrowserWebview(webPreferences, params)) { + event.preventDefault() + return + } + const browserSession = session.fromPartition(params.partition) + if (!securedBrowserSessions.has(browserSession)) { + securedBrowserSessions.add(browserSession) + configureBrowserPermissionHandlers(browserSession) + browserSession.on("will-download", (downloadEvent) => downloadEvent.preventDefault()) + } + }) + window.webContents.on("did-attach-webview", (_event, guest) => { + browserController.observeGuest(window.webContents, guest) + const guardNavigation = (event: Electron.Event, url: string) => { + if (!isBrowserUrlAllowed(url)) event.preventDefault() + } + guest.on("will-navigate", guardNavigation) + guest.on("will-redirect", guardNavigation) + guest.setWindowOpenHandler(({ url }) => { + if (isBrowserUrlAllowed(url)) queueMicrotask(() => { + try { + if (!guest.isDestroyed()) void guest.loadURL(url).catch(() => undefined) + } catch {} + }) + return { action: "deny" } + }) + }) window.webContents.on("did-start-navigation", (_event, _url, _isInPlace, isMainFrame) => { if (isMainFrame) setWorkspaceMenuEnabled(window, false) }) @@ -188,6 +226,7 @@ function runPrimary(firstIntent: LaunchIntent) { clearWorkspaceMenuWindow(window) remoteOrigins.delete(window.id) insecureOrigins.delete(window.webContents.id) + browserController.removeOwner(window.webContents) }) if (isMac) window.webContents.session.setSpellCheckerEnabled(false) if (process.env.NODE_ENV === "development") window.webContents.openDevTools({ mode: "detach" }) @@ -224,6 +263,7 @@ function runPrimary(firstIntent: LaunchIntent) { openRemoteWindow, newWindow: () => intentQueue.enqueue({ newWindow: true, folders: [] }), nextFolder: (id) => registry.nextFolder(id), acknowledgeFolder: (id, folder, opened) => registry.acknowledgeFolder(id, folder, opened), developerRunManager, + browserController, }) lifecycle.registerAppEvents() app.on("second-instance", (_event, argv, workingDirectory) => { diff --git a/packages/electron-app/electron/main/permissions.ts b/packages/electron-app/electron/main/permissions.ts index 4a785621c..4fae3e059 100644 --- a/packages/electron-app/electron/main/permissions.ts +++ b/packages/electron-app/electron/main/permissions.ts @@ -34,6 +34,12 @@ export function configureMediaPermissionHandlers(getAllowedOrigins: () => string }) } +export function configureBrowserPermissionHandlers(targetSession: Session) { + targetSession.setPermissionCheckHandler(() => false) + targetSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false)) + targetSession.setDevicePermissionHandler(() => false) +} + export async function requestMicrophoneAccess(): Promise { if (!isMac) { return true diff --git a/packages/electron-app/electron/preload/index.cjs b/packages/electron-app/electron/preload/index.cjs index 83ff8a904..5e80ac23b 100644 --- a/packages/electron-app/electron/preload/index.cjs +++ b/packages/electron-app/electron/preload/index.cjs @@ -78,6 +78,14 @@ const localElectronAPI = { ipcRenderer.on("developer-run:log", handler) return () => ipcRenderer.removeListener("developer-run:log", handler) }, + registerBrowserTarget: (payload) => ipcRenderer.invoke("browser-target:register", payload), + unregisterBrowserTarget: (registrationId) => ipcRenderer.invoke("browser-target:unregister", registrationId), + claimBrowserOpen: (requestId) => ipcRenderer.invoke("browser-target:claimOpen", requestId), + onBrowserOpenRequest: (callback) => { + const handler = (_event, payload) => callback(payload) + ipcRenderer.on("browser-target:open", handler) + return () => ipcRenderer.removeListener("browser-target:open", handler) + }, } const remoteElectronAPI = { diff --git a/packages/electron-app/electron/preload/index.test.ts b/packages/electron-app/electron/preload/index.test.ts index cdc594abb..a6c53900e 100644 --- a/packages/electron-app/electron/preload/index.test.ts +++ b/packages/electron-app/electron/preload/index.test.ts @@ -29,6 +29,7 @@ test("CLI event disposers remove only their own wrapper listeners", () => { ["onCliError", "cli:error"], ["onDeveloperRunStatus", "developer-run:status"], ["onDeveloperRunLog", "developer-run:log"], + ["onBrowserOpenRequest", "browser-target:open"], ] as const) { const calls: string[] = [] const disposeFirst = api![subscribe]((value: string) => calls.push(`first:${value}`)) diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index 9df743099..cf90c4127 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -24,7 +24,7 @@ "prebuild": "npm run prepare:resources", "build": "electron-vite build", "typecheck": "tsc --noEmit -p tsconfig.json", - "test:native": "node --import tsx --test electron/main/client-state-cross-host.test.ts electron/main/client-state-process.test.ts electron/main/client-state.test.ts electron/main/client-state-ipc.test.ts electron/main/client-state-navigation.test.ts electron/main/developer-run-manager.test.ts electron/main/local-window-registry.test.ts electron/main/menu-target.test.ts electron/main/multiwindow-lifecycle.test.ts electron/main/native-request.test.ts electron/main/navigation-security.test.ts electron/main/process-exit.test.ts electron/main/process-output.test.ts electron/main/process-stop.test.ts electron/main/remote-window-registry.test.ts electron/main/renderer-client-state-flush.test.ts electron/main/renderer-origin.test.ts electron/main/serialized-lifecycle.test.ts electron/main/startup.test.ts electron/main/window-state.test.ts electron/main/workspace-open.test.ts electron/preload/index.test.ts", + "test:native": "node --import tsx --test electron/main/browser-controller.test.ts electron/main/browser-webview-security.test.ts electron/main/client-state-cross-host.test.ts electron/main/client-state-process.test.ts electron/main/client-state.test.ts electron/main/client-state-ipc.test.ts electron/main/client-state-navigation.test.ts electron/main/developer-run-manager.test.ts electron/main/local-window-registry.test.ts electron/main/menu-target.test.ts electron/main/multiwindow-lifecycle.test.ts electron/main/native-request.test.ts electron/main/navigation-security.test.ts electron/main/process-exit.test.ts electron/main/process-output.test.ts electron/main/process-stop.test.ts electron/main/remote-window-registry.test.ts electron/main/renderer-client-state-flush.test.ts electron/main/renderer-origin.test.ts electron/main/serialized-lifecycle.test.ts electron/main/startup.test.ts electron/main/window-state.test.ts electron/main/workspace-open.test.ts electron/preload/index.test.ts", "preview": "electron-vite preview", "build:binaries": "node scripts/build.js", "build:mac": "node scripts/build.js mac", diff --git a/packages/server/src/opencode/automation-plugin.test.ts b/packages/server/src/opencode/automation-plugin.test.ts index ba5622a49..f95b2b3ab 100644 --- a/packages/server/src/opencode/automation-plugin.test.ts +++ b/packages/server/src/opencode/automation-plugin.test.ts @@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import os from "node:os" import path from "node:path" import test from "node:test" -import { installAutomationPlugin, parseDeveloperAction, setupAutomationPlugin } from "./automation-plugin" +import { installAutomationPlugin, parseBrowserAction, parseDeveloperAction, setupAutomationPlugin } from "./automation-plugin" test("validates developer automation actions", () => { assert.deepEqual(parseDeveloperAction({ action: "type", ref: "e4", text: "CodeNomad" }), { @@ -15,6 +15,20 @@ test("validates developer automation actions", () => { assert.throws(() => parseDeveloperAction({ action: "click" }), /click requires ref/) }) +test("validates browser actions", () => { + assert.deepEqual(parseBrowserAction({ action: "open", url: "https://example.com" }), { + action: "open", + url: "https://example.com", + }) + assert.deepEqual(parseBrowserAction({ action: "type", ref: "e2", text: "hello", clear: false }), { + action: "type", + ref: "e2", + text: "hello", + clear: false, + }) + assert.throws(() => parseBrowserAction({ action: "open" }), /open requires url/) +}) + test("registers developer tools while execution remains session-gated", async () => { let skill: Record | undefined const tools: string[] = [] @@ -27,7 +41,8 @@ test("registers developer tools while execution remains session-gated", async () assert.equal(skill?.id, "codenomad-automation") assert.equal(skill?.autoinvoke, true) assert.match(String(skill?.content), /codenomad\.inspect/) - assert.deepEqual(tools, ["inspect", "act", "screenshot"]) + assert.match(String(skill?.content), /codenomad\.browser/) + assert.deepEqual(tools, ["inspect", "act", "screenshot", "browser"]) }) diff --git a/packages/server/src/opencode/automation-plugin.ts b/packages/server/src/opencode/automation-plugin.ts index 208370071..f28f73edc 100644 --- a/packages/server/src/opencode/automation-plugin.ts +++ b/packages/server/src/opencode/automation-plugin.ts @@ -12,6 +12,8 @@ const SKILL_ID = "codenomad-automation" const SKILL_CONTENT = `# CodeNomad Automation When a desktop build is connected in Advanced Settings, use \`codenomad.inspect\` to read its accessibility tree, runtime diagnostics, and recent launch logs. Use refs only from the latest inspection with \`codenomad.act\`, and use \`codenomad.screenshot\` for visual evidence. After changing the desktop implementation, call \`codenomad.act\` with \`restart\`, wait for readiness, and inspect again. + +Use \`codenomad.browser\` when a task requires inspecting or testing a website in the visible session preview. Open or navigate to an HTTP(S) URL, take a snapshot before interacting, use refs only from the latest snapshot, and take another snapshot to verify the result. Do not perform consequential actions unless the user explicitly requests them. ` export type DeveloperAction = @@ -21,6 +23,14 @@ export type DeveloperAction = | { action: "restart" } | { action: "screenshot" } +export type BrowserAction = + | { action: "open"; url: string } + | { action: "navigate"; url: string } + | { action: "snapshot" } + | { action: "click"; ref: string } + | { action: "type"; ref: string; text: string; clear?: boolean } + | { action: "screenshot" } + export interface AutomationBridgeRegistration { version: 1 url: string @@ -134,6 +144,28 @@ export function parseDeveloperAction(input: unknown): DeveloperAction { } } +export function parseBrowserAction(input: unknown): BrowserAction { + if (!input || typeof input !== "object") throw new Error("Browser input must be an object") + const value = input as Record + switch (value.action) { + case "open": + case "navigate": + if (typeof value.url !== "string") throw new Error(`${value.action} requires url`) + return { action: value.action, url: value.url } + case "snapshot": + case "screenshot": + return { action: value.action } + case "click": + if (typeof value.ref !== "string") throw new Error("click requires ref from the latest snapshot") + return { action: "click", ref: value.ref } + case "type": + if (typeof value.ref !== "string" || typeof value.text !== "string") throw new Error("type requires ref and text") + return { action: "type", ref: value.ref, text: value.text, clear: value.clear !== false } + default: + throw new Error("Unsupported browser action") + } +} + async function registrations(): Promise { const directory = automationBridgeDirectory() const names = await readdir(directory).catch(() => [] as string[]) @@ -168,12 +200,12 @@ async function callBridge( return { status: response.status, body: await response.json() as BridgeResponse } } -function formatBridgeResult(resultValue: unknown) { +function formatBridgeResult(resultValue: unknown, screenshot = { text: "Captured the connected CodeNomad build.", name: "codenomad.png" }) { const result = resultValue as { image?: { data: string; mime: string }; [key: string]: unknown } | undefined if (result?.image) { const content: Array> = [ - { type: "text", text: "Captured the connected CodeNomad build." }, - { type: "file", uri: `data:${result.image.mime};base64,${result.image.data}`, mime: result.image.mime, name: "codenomad.png" }, + { type: "text", text: screenshot.text }, + { type: "file", uri: `data:${result.image.mime};base64,${result.image.data}`, mime: result.image.mime, name: screenshot.name }, ] return { content, @@ -182,6 +214,33 @@ function formatBridgeResult(resultValue: unknown) { return { content: JSON.stringify(result ?? null, null, 2) } } +async function executeBrowserTool(sessionID: string, input: unknown) { + const command = parseBrowserAction(input) + const active = await registrations() + const probes = await Promise.all(active.map(async (registration) => { + try { + const mode = command.action === "open" ? "browser-claim" : "browser-probe" + return (await callBridge(registration, { mode, sessionID })).status === 200 ? registration : undefined + } catch { + return undefined + } + })) + const targets = probes.filter((value): value is AutomationBridgeRegistration => Boolean(value)) + if (targets.length === 0) { + throw new Error(command.action === "open" + ? "No local CodeNomad window owns this session" + : "No visible local CodeNomad browser is attached to this session") + } + if (targets.length > 1) { + throw new Error(command.action === "open" + ? "Multiple CodeNomad instances own this session" + : "Multiple CodeNomad windows expose a browser for this session") + } + const response = await callBridge(targets[0], { mode: "browser-execute", sessionID, command }, REQUEST_TIMEOUT_MS) + if (response.status !== 200) throw new Error(response.body.error || `Browser command failed (${response.status})`) + return formatBridgeResult(response.body.result, { text: "Captured the visible CodeNomad browser.", name: "browser.png" }) +} + async function executeDeveloperTool(sessionID: string, command: DeveloperAction) { const active = await registrations() const probes = await Promise.all(active.map(async (registration) => { @@ -243,6 +302,24 @@ export async function setupAutomationPlugin(context: AutomationPluginContext): P options: { namespace: "codenomad", codemode: false }, execute: (_input, tool) => executeDeveloperTool(tool.sessionID, { action: "screenshot" }), }) + draft.add({ + name: "browser", + description: "Open and control the browser attached to the current CodeNomad session. Use open when the preview is closed, then snapshot before click or type.", + input: { + type: "object", + properties: { + action: { type: "string", enum: ["open", "navigate", "snapshot", "click", "type", "screenshot"] }, + url: { type: "string", description: "HTTP(S) URL for open or navigate" }, + ref: { type: "string", description: "Element ref from the latest snapshot" }, + text: { type: "string", description: "Text for type" }, + clear: { type: "boolean", description: "Clear the field before typing (default true)" }, + }, + required: ["action"], + additionalProperties: false, + }, + options: { namespace: "codenomad", codemode: false }, + execute: (input, tool) => executeBrowserTool(tool.sessionID, input), + }) }) } diff --git a/packages/server/src/server/routes/automation-plugin.test.ts b/packages/server/src/server/routes/automation-plugin.test.ts index 0d1ce900e..1574bf72f 100644 --- a/packages/server/src/server/routes/automation-plugin.test.ts +++ b/packages/server/src/server/routes/automation-plugin.test.ts @@ -72,3 +72,44 @@ test("fences developer automation by owned session and forwards CDP actions", as assert.equal(unauthorized.statusCode, 401) await app.close() }) + +test("claims owned sessions and forwards browser commands through the automation bridge", async () => { + const app = Fastify({ logger: false }) + const nativeCalls: Array<{ method: string; params: unknown }> = [] + registerAutomationPluginRoute(app, { + authManager: { isLoopbackRequest: () => true }, + bridgeToken: "secret", + nativeParent: { + request: async (method: string, params: unknown) => { + nativeCalls.push({ method, params }) + return method === "browser.probe" ? { available: true } : { url: "https://example.com/" } + }, + }, + workspaceManager: { + getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: "D:\\project" } }) } }), + list: () => [{ id: "workspace-1" }], + ownsLocation: async () => true, + }, + } as never) + + const request = async (body: Record) => app.inject({ + method: "POST", + url: AUTOMATION_BRIDGE_PATH, + headers: { "x-codenomad-automation-token": "secret" }, + payload: body, + }) + + assert.equal((await request({ mode: "browser-claim", sessionID: "session-1" })).statusCode, 200) + assert.equal(nativeCalls.length, 0) + assert.equal((await request({ mode: "browser-probe", sessionID: "session-1" })).statusCode, 200) + const open = await request({ mode: "browser-execute", sessionID: "session-1", command: { action: "open", url: "https://example.com" } }) + assert.equal(open.statusCode, 200) + assert.deepEqual(nativeCalls, [ + { method: "browser.probe", params: { sessionID: "session-1" } }, + { + method: "browser.execute", + params: { sessionID: "session-1", command: { action: "open", url: "https://example.com" } }, + }, + ]) + await app.close() +}) diff --git a/packages/server/src/server/routes/automation-plugin.ts b/packages/server/src/server/routes/automation-plugin.ts index 1dd01370b..8caddec1f 100644 --- a/packages/server/src/server/routes/automation-plugin.ts +++ b/packages/server/src/server/routes/automation-plugin.ts @@ -3,7 +3,7 @@ import type { FastifyInstance, FastifyRequest } from "fastify" import type { AuthManager } from "../../auth/manager" import type { DeveloperCdp } from "../../developer-cdp" import type { NativeParent } from "../../native-parent" -import { AUTOMATION_BRIDGE_PATH, parseDeveloperAction } from "../../opencode/automation-plugin" +import { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from "../../opencode/automation-plugin" import type { WorkspaceManager } from "../../workspaces/manager" interface AutomationPluginRouteDeps { @@ -42,7 +42,7 @@ export function registerAutomationPluginRoute(app: FastifyInstance, deps: Automa app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => { if (!isAutomationPluginRequest(request, deps)) return reply.code(401).send({ error: "Unauthorized automation bridge" }) const body = request.body as { mode?: unknown; sessionID?: unknown; command?: unknown } | undefined - if (!body || !["developer-probe", "developer-execute"].includes(String(body.mode)) + if (!body || !["developer-probe", "developer-execute", "browser-claim", "browser-probe", "browser-execute"].includes(String(body.mode)) || typeof body.sessionID !== "string" || body.sessionID.length > 256) { return reply.code(400).send({ error: "Invalid automation bridge request" }) } @@ -58,6 +58,30 @@ export function registerAutomationPluginRoute(app: FastifyInstance, deps: Automa ))).some(Boolean) if (!owned) return reply.code(404).send({ error: "Session is not owned by this CodeNomad instance" }) + if (body.mode === "browser-claim") return reply.send({ result: { available: true } }) + if (body.mode === "browser-probe") { + try { + const result = await deps.nativeParent.request<{ available: boolean }>("browser.probe", { sessionID: body.sessionID }) + return result.available ? reply.send({ result }) : reply.code(404).send({ error: "No visible browser target" }) + } catch (error) { + return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) }) + } + } + if (body.mode === "browser-execute") { + let command + try { + command = parseBrowserAction(body.command) + } catch (error) { + return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) }) + } + try { + const result = await deps.nativeParent.request("browser.execute", { sessionID: body.sessionID, command }) + return reply.send({ result }) + } catch (error) { + return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) }) + } + } + let native: DeveloperNativeStatus try { native = await deps.nativeParent.request("developer.status", {}) diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index 64046e6ef..3360cc6cd 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" tauri-build = { version = "2.5.2", features = [] } [dependencies] -tauri = { version = "2.5.2", features = [ "devtools"] } +tauri = { version = "2.5.2", features = ["devtools", "unstable"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" diff --git a/packages/tauri-app/src-tauri/build.rs b/packages/tauri-app/src-tauri/build.rs index dff363104..66615f4ce 100644 --- a/packages/tauri-app/src-tauri/build.rs +++ b/packages/tauri-app/src-tauri/build.rs @@ -60,6 +60,11 @@ fn main() { "developer_run_get", "developer_run_start", "developer_run_stop", + "browser_target_register", + "browser_target_update", + "browser_target_action", + "browser_target_unregister", + "browser_target_claim_open", ]), )) .expect("build Tauri application and command ACL") diff --git a/packages/tauri-app/src-tauri/capabilities/main-window.json b/packages/tauri-app/src-tauri/capabilities/main-window.json index 084910cf9..986f1316b 100644 --- a/packages/tauri-app/src-tauri/capabilities/main-window.json +++ b/packages/tauri-app/src-tauri/capabilities/main-window.json @@ -46,6 +46,11 @@ "allow-set-workspace-menu-enabled", "allow-developer-run-get", "allow-developer-run-start", - "allow-developer-run-stop" + "allow-developer-run-stop", + "allow-browser-target-register", + "allow-browser-target-update", + "allow-browser-target-action", + "allow-browser-target-unregister", + "allow-browser-target-claim-open" ] } diff --git a/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json b/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json index 0783517cf..00e542024 100644 --- a/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json +++ b/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"__app-acl__":{"default_permission":null,"permissions":{"allow-cli-get-status":{"identifier":"allow-cli-get-status","description":"Enables the cli_get_status command without any pre-configured scope.","commands":{"allow":["cli_get_status"],"deny":[]}},"allow-cli-restart":{"identifier":"allow-cli-restart","description":"Enables the cli_restart command without any pre-configured scope.","commands":{"allow":["cli_restart"],"deny":[]}},"allow-client-state-claim-access":{"identifier":"allow-client-state-claim-access","description":"Enables the client_state_claim_access command without any pre-configured scope.","commands":{"allow":["client_state_claim_access"],"deny":[]}},"allow-client-state-clear":{"identifier":"allow-client-state-clear","description":"Enables the client_state_clear command without any pre-configured scope.","commands":{"allow":["client_state_clear"],"deny":[]}},"allow-client-state-commit-partitions":{"identifier":"allow-client-state-commit-partitions","description":"Enables the client_state_commit_partitions command without any pre-configured scope.","commands":{"allow":["client_state_commit_partitions"],"deny":[]}},"allow-client-state-load":{"identifier":"allow-client-state-load","description":"Enables the client_state_load command without any pre-configured scope.","commands":{"allow":["client_state_load"],"deny":[]}},"allow-client-state-load-partition":{"identifier":"allow-client-state-load-partition","description":"Enables the client_state_load_partition command without any pre-configured scope.","commands":{"allow":["client_state_load_partition"],"deny":[]}},"allow-client-state-navigation-flushed":{"identifier":"allow-client-state-navigation-flushed","description":"Enables the client_state_navigation_flushed command without any pre-configured scope.","commands":{"allow":["client_state_navigation_flushed"],"deny":[]}},"allow-client-state-renderer-flushed":{"identifier":"allow-client-state-renderer-flushed","description":"Enables the client_state_renderer_flushed command without any pre-configured scope.","commands":{"allow":["client_state_renderer_flushed"],"deny":[]}},"allow-client-state-save":{"identifier":"allow-client-state-save","description":"Enables the client_state_save command without any pre-configured scope.","commands":{"allow":["client_state_save"],"deny":[]}},"allow-client-state-set-restore-enabled":{"identifier":"allow-client-state-set-restore-enabled","description":"Enables the client_state_set_restore_enabled command without any pre-configured scope.","commands":{"allow":["client_state_set_restore_enabled"],"deny":[]}},"allow-desktop-launch-acknowledge-folder":{"identifier":"allow-desktop-launch-acknowledge-folder","description":"Enables the desktop_launch_acknowledge_folder command without any pre-configured scope.","commands":{"allow":["desktop_launch_acknowledge_folder"],"deny":[]}},"allow-desktop-launch-next-folder":{"identifier":"allow-desktop-launch-next-folder","description":"Enables the desktop_launch_next_folder command without any pre-configured scope.","commands":{"allow":["desktop_launch_next_folder"],"deny":[]}},"allow-desktop-launch-ready":{"identifier":"allow-desktop-launch-ready","description":"Enables the desktop_launch_ready command without any pre-configured scope.","commands":{"allow":["desktop_launch_ready"],"deny":[]}},"allow-developer-run-get":{"identifier":"allow-developer-run-get","description":"Enables the developer_run_get command without any pre-configured scope.","commands":{"allow":["developer_run_get"],"deny":[]}},"allow-developer-run-start":{"identifier":"allow-developer-run-start","description":"Enables the developer_run_start command without any pre-configured scope.","commands":{"allow":["developer_run_start"],"deny":[]}},"allow-developer-run-stop":{"identifier":"allow-developer-run-stop","description":"Enables the developer_run_stop command without any pre-configured scope.","commands":{"allow":["developer_run_stop"],"deny":[]}},"allow-install-stable-update":{"identifier":"allow-install-stable-update","description":"Enables the install_stable_update command without any pre-configured scope.","commands":{"allow":["install_stable_update"],"deny":[]}},"allow-needs-local-certificate-install":{"identifier":"allow-needs-local-certificate-install","description":"Enables the needs_local_certificate_install command without any pre-configured scope.","commands":{"allow":["needs_local_certificate_install"],"deny":[]}},"allow-open-remote-window":{"identifier":"allow-open-remote-window","description":"Enables the open_remote_window command without any pre-configured scope.","commands":{"allow":["open_remote_window"],"deny":[]}},"allow-open-workspace-target":{"identifier":"allow-open-workspace-target","description":"Enables the open_workspace_target command without any pre-configured scope.","commands":{"allow":["open_workspace_target"],"deny":[]}},"allow-set-workspace-menu-enabled":{"identifier":"allow-set-workspace-menu-enabled","description":"Enables the set_workspace_menu_enabled command without any pre-configured scope.","commands":{"allow":["set_workspace_menu_enabled"],"deny":[]}},"allow-wake-lock-start":{"identifier":"allow-wake-lock-start","description":"Enables the wake_lock_start command without any pre-configured scope.","commands":{"allow":["wake_lock_start"],"deny":[]}},"allow-wake-lock-stop":{"identifier":"allow-wake-lock-stop","description":"Enables the wake_lock_stop command without any pre-configured scope.","commands":{"allow":["wake_lock_stop"],"deny":[]}},"deny-cli-get-status":{"identifier":"deny-cli-get-status","description":"Denies the cli_get_status command without any pre-configured scope.","commands":{"allow":[],"deny":["cli_get_status"]}},"deny-cli-restart":{"identifier":"deny-cli-restart","description":"Denies the cli_restart command without any pre-configured scope.","commands":{"allow":[],"deny":["cli_restart"]}},"deny-client-state-claim-access":{"identifier":"deny-client-state-claim-access","description":"Denies the client_state_claim_access command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_claim_access"]}},"deny-client-state-clear":{"identifier":"deny-client-state-clear","description":"Denies the client_state_clear command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_clear"]}},"deny-client-state-commit-partitions":{"identifier":"deny-client-state-commit-partitions","description":"Denies the client_state_commit_partitions command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_commit_partitions"]}},"deny-client-state-load":{"identifier":"deny-client-state-load","description":"Denies the client_state_load command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_load"]}},"deny-client-state-load-partition":{"identifier":"deny-client-state-load-partition","description":"Denies the client_state_load_partition command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_load_partition"]}},"deny-client-state-navigation-flushed":{"identifier":"deny-client-state-navigation-flushed","description":"Denies the client_state_navigation_flushed command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_navigation_flushed"]}},"deny-client-state-renderer-flushed":{"identifier":"deny-client-state-renderer-flushed","description":"Denies the client_state_renderer_flushed command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_renderer_flushed"]}},"deny-client-state-save":{"identifier":"deny-client-state-save","description":"Denies the client_state_save command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_save"]}},"deny-client-state-set-restore-enabled":{"identifier":"deny-client-state-set-restore-enabled","description":"Denies the client_state_set_restore_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_set_restore_enabled"]}},"deny-desktop-launch-acknowledge-folder":{"identifier":"deny-desktop-launch-acknowledge-folder","description":"Denies the desktop_launch_acknowledge_folder command without any pre-configured scope.","commands":{"allow":[],"deny":["desktop_launch_acknowledge_folder"]}},"deny-desktop-launch-next-folder":{"identifier":"deny-desktop-launch-next-folder","description":"Denies the desktop_launch_next_folder command without any pre-configured scope.","commands":{"allow":[],"deny":["desktop_launch_next_folder"]}},"deny-desktop-launch-ready":{"identifier":"deny-desktop-launch-ready","description":"Denies the desktop_launch_ready command without any pre-configured scope.","commands":{"allow":[],"deny":["desktop_launch_ready"]}},"deny-developer-run-get":{"identifier":"deny-developer-run-get","description":"Denies the developer_run_get command without any pre-configured scope.","commands":{"allow":[],"deny":["developer_run_get"]}},"deny-developer-run-start":{"identifier":"deny-developer-run-start","description":"Denies the developer_run_start command without any pre-configured scope.","commands":{"allow":[],"deny":["developer_run_start"]}},"deny-developer-run-stop":{"identifier":"deny-developer-run-stop","description":"Denies the developer_run_stop command without any pre-configured scope.","commands":{"allow":[],"deny":["developer_run_stop"]}},"deny-install-stable-update":{"identifier":"deny-install-stable-update","description":"Denies the install_stable_update command without any pre-configured scope.","commands":{"allow":[],"deny":["install_stable_update"]}},"deny-needs-local-certificate-install":{"identifier":"deny-needs-local-certificate-install","description":"Denies the needs_local_certificate_install command without any pre-configured scope.","commands":{"allow":[],"deny":["needs_local_certificate_install"]}},"deny-open-remote-window":{"identifier":"deny-open-remote-window","description":"Denies the open_remote_window command without any pre-configured scope.","commands":{"allow":[],"deny":["open_remote_window"]}},"deny-open-workspace-target":{"identifier":"deny-open-workspace-target","description":"Denies the open_workspace_target command without any pre-configured scope.","commands":{"allow":[],"deny":["open_workspace_target"]}},"deny-set-workspace-menu-enabled":{"identifier":"deny-set-workspace-menu-enabled","description":"Denies the set_workspace_menu_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_workspace_menu_enabled"]}},"deny-wake-lock-start":{"identifier":"deny-wake-lock-start","description":"Denies the wake_lock_start command without any pre-configured scope.","commands":{"allow":[],"deny":["wake_lock_start"]}},"deny-wake-lock-stop":{"identifier":"deny-wake-lock-stop","description":"Denies the wake_lock_stop command without any pre-configured scope.","commands":{"allow":[],"deny":["wake_lock_stop"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","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","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"global-shortcut":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n","permissions":[]},"permissions":{"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-register-all":{"identifier":"allow-register-all","description":"Enables the register_all command without any pre-configured scope.","commands":{"allow":["register_all"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"allow-unregister-all":{"identifier":"allow-unregister-all","description":"Enables the unregister_all command without any pre-configured scope.","commands":{"allow":["unregister_all"],"deny":[]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-register-all":{"identifier":"deny-register-all","description":"Denies the register_all command without any pre-configured scope.","commands":{"allow":[],"deny":["register_all"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}},"deny-unregister-all":{"identifier":"deny-unregister-all","description":"Denies the unregister_all command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister_all"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","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","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","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","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"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"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"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"}},"required":["path"],"type":"object"}],"definitions":{"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"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}}} \ No newline at end of file +{"__app-acl__":{"default_permission":null,"permissions":{"allow-browser-target-action":{"identifier":"allow-browser-target-action","description":"Enables the browser_target_action command without any pre-configured scope.","commands":{"allow":["browser_target_action"],"deny":[]}},"allow-browser-target-claim-open":{"identifier":"allow-browser-target-claim-open","description":"Enables the browser_target_claim_open command without any pre-configured scope.","commands":{"allow":["browser_target_claim_open"],"deny":[]}},"allow-browser-target-register":{"identifier":"allow-browser-target-register","description":"Enables the browser_target_register command without any pre-configured scope.","commands":{"allow":["browser_target_register"],"deny":[]}},"allow-browser-target-unregister":{"identifier":"allow-browser-target-unregister","description":"Enables the browser_target_unregister command without any pre-configured scope.","commands":{"allow":["browser_target_unregister"],"deny":[]}},"allow-browser-target-update":{"identifier":"allow-browser-target-update","description":"Enables the browser_target_update command without any pre-configured scope.","commands":{"allow":["browser_target_update"],"deny":[]}},"allow-cli-get-status":{"identifier":"allow-cli-get-status","description":"Enables the cli_get_status command without any pre-configured scope.","commands":{"allow":["cli_get_status"],"deny":[]}},"allow-cli-restart":{"identifier":"allow-cli-restart","description":"Enables the cli_restart command without any pre-configured scope.","commands":{"allow":["cli_restart"],"deny":[]}},"allow-client-state-claim-access":{"identifier":"allow-client-state-claim-access","description":"Enables the client_state_claim_access command without any pre-configured scope.","commands":{"allow":["client_state_claim_access"],"deny":[]}},"allow-client-state-clear":{"identifier":"allow-client-state-clear","description":"Enables the client_state_clear command without any pre-configured scope.","commands":{"allow":["client_state_clear"],"deny":[]}},"allow-client-state-commit-partitions":{"identifier":"allow-client-state-commit-partitions","description":"Enables the client_state_commit_partitions command without any pre-configured scope.","commands":{"allow":["client_state_commit_partitions"],"deny":[]}},"allow-client-state-load":{"identifier":"allow-client-state-load","description":"Enables the client_state_load command without any pre-configured scope.","commands":{"allow":["client_state_load"],"deny":[]}},"allow-client-state-load-partition":{"identifier":"allow-client-state-load-partition","description":"Enables the client_state_load_partition command without any pre-configured scope.","commands":{"allow":["client_state_load_partition"],"deny":[]}},"allow-client-state-navigation-flushed":{"identifier":"allow-client-state-navigation-flushed","description":"Enables the client_state_navigation_flushed command without any pre-configured scope.","commands":{"allow":["client_state_navigation_flushed"],"deny":[]}},"allow-client-state-renderer-flushed":{"identifier":"allow-client-state-renderer-flushed","description":"Enables the client_state_renderer_flushed command without any pre-configured scope.","commands":{"allow":["client_state_renderer_flushed"],"deny":[]}},"allow-client-state-save":{"identifier":"allow-client-state-save","description":"Enables the client_state_save command without any pre-configured scope.","commands":{"allow":["client_state_save"],"deny":[]}},"allow-client-state-set-restore-enabled":{"identifier":"allow-client-state-set-restore-enabled","description":"Enables the client_state_set_restore_enabled command without any pre-configured scope.","commands":{"allow":["client_state_set_restore_enabled"],"deny":[]}},"allow-desktop-launch-acknowledge-folder":{"identifier":"allow-desktop-launch-acknowledge-folder","description":"Enables the desktop_launch_acknowledge_folder command without any pre-configured scope.","commands":{"allow":["desktop_launch_acknowledge_folder"],"deny":[]}},"allow-desktop-launch-next-folder":{"identifier":"allow-desktop-launch-next-folder","description":"Enables the desktop_launch_next_folder command without any pre-configured scope.","commands":{"allow":["desktop_launch_next_folder"],"deny":[]}},"allow-desktop-launch-ready":{"identifier":"allow-desktop-launch-ready","description":"Enables the desktop_launch_ready command without any pre-configured scope.","commands":{"allow":["desktop_launch_ready"],"deny":[]}},"allow-developer-run-get":{"identifier":"allow-developer-run-get","description":"Enables the developer_run_get command without any pre-configured scope.","commands":{"allow":["developer_run_get"],"deny":[]}},"allow-developer-run-start":{"identifier":"allow-developer-run-start","description":"Enables the developer_run_start command without any pre-configured scope.","commands":{"allow":["developer_run_start"],"deny":[]}},"allow-developer-run-stop":{"identifier":"allow-developer-run-stop","description":"Enables the developer_run_stop command without any pre-configured scope.","commands":{"allow":["developer_run_stop"],"deny":[]}},"allow-install-stable-update":{"identifier":"allow-install-stable-update","description":"Enables the install_stable_update command without any pre-configured scope.","commands":{"allow":["install_stable_update"],"deny":[]}},"allow-needs-local-certificate-install":{"identifier":"allow-needs-local-certificate-install","description":"Enables the needs_local_certificate_install command without any pre-configured scope.","commands":{"allow":["needs_local_certificate_install"],"deny":[]}},"allow-open-remote-window":{"identifier":"allow-open-remote-window","description":"Enables the open_remote_window command without any pre-configured scope.","commands":{"allow":["open_remote_window"],"deny":[]}},"allow-open-workspace-target":{"identifier":"allow-open-workspace-target","description":"Enables the open_workspace_target command without any pre-configured scope.","commands":{"allow":["open_workspace_target"],"deny":[]}},"allow-set-workspace-menu-enabled":{"identifier":"allow-set-workspace-menu-enabled","description":"Enables the set_workspace_menu_enabled command without any pre-configured scope.","commands":{"allow":["set_workspace_menu_enabled"],"deny":[]}},"allow-wake-lock-start":{"identifier":"allow-wake-lock-start","description":"Enables the wake_lock_start command without any pre-configured scope.","commands":{"allow":["wake_lock_start"],"deny":[]}},"allow-wake-lock-stop":{"identifier":"allow-wake-lock-stop","description":"Enables the wake_lock_stop command without any pre-configured scope.","commands":{"allow":["wake_lock_stop"],"deny":[]}},"deny-browser-target-action":{"identifier":"deny-browser-target-action","description":"Denies the browser_target_action command without any pre-configured scope.","commands":{"allow":[],"deny":["browser_target_action"]}},"deny-browser-target-claim-open":{"identifier":"deny-browser-target-claim-open","description":"Denies the browser_target_claim_open command without any pre-configured scope.","commands":{"allow":[],"deny":["browser_target_claim_open"]}},"deny-browser-target-register":{"identifier":"deny-browser-target-register","description":"Denies the browser_target_register command without any pre-configured scope.","commands":{"allow":[],"deny":["browser_target_register"]}},"deny-browser-target-unregister":{"identifier":"deny-browser-target-unregister","description":"Denies the browser_target_unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["browser_target_unregister"]}},"deny-browser-target-update":{"identifier":"deny-browser-target-update","description":"Denies the browser_target_update command without any pre-configured scope.","commands":{"allow":[],"deny":["browser_target_update"]}},"deny-cli-get-status":{"identifier":"deny-cli-get-status","description":"Denies the cli_get_status command without any pre-configured scope.","commands":{"allow":[],"deny":["cli_get_status"]}},"deny-cli-restart":{"identifier":"deny-cli-restart","description":"Denies the cli_restart command without any pre-configured scope.","commands":{"allow":[],"deny":["cli_restart"]}},"deny-client-state-claim-access":{"identifier":"deny-client-state-claim-access","description":"Denies the client_state_claim_access command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_claim_access"]}},"deny-client-state-clear":{"identifier":"deny-client-state-clear","description":"Denies the client_state_clear command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_clear"]}},"deny-client-state-commit-partitions":{"identifier":"deny-client-state-commit-partitions","description":"Denies the client_state_commit_partitions command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_commit_partitions"]}},"deny-client-state-load":{"identifier":"deny-client-state-load","description":"Denies the client_state_load command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_load"]}},"deny-client-state-load-partition":{"identifier":"deny-client-state-load-partition","description":"Denies the client_state_load_partition command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_load_partition"]}},"deny-client-state-navigation-flushed":{"identifier":"deny-client-state-navigation-flushed","description":"Denies the client_state_navigation_flushed command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_navigation_flushed"]}},"deny-client-state-renderer-flushed":{"identifier":"deny-client-state-renderer-flushed","description":"Denies the client_state_renderer_flushed command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_renderer_flushed"]}},"deny-client-state-save":{"identifier":"deny-client-state-save","description":"Denies the client_state_save command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_save"]}},"deny-client-state-set-restore-enabled":{"identifier":"deny-client-state-set-restore-enabled","description":"Denies the client_state_set_restore_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["client_state_set_restore_enabled"]}},"deny-desktop-launch-acknowledge-folder":{"identifier":"deny-desktop-launch-acknowledge-folder","description":"Denies the desktop_launch_acknowledge_folder command without any pre-configured scope.","commands":{"allow":[],"deny":["desktop_launch_acknowledge_folder"]}},"deny-desktop-launch-next-folder":{"identifier":"deny-desktop-launch-next-folder","description":"Denies the desktop_launch_next_folder command without any pre-configured scope.","commands":{"allow":[],"deny":["desktop_launch_next_folder"]}},"deny-desktop-launch-ready":{"identifier":"deny-desktop-launch-ready","description":"Denies the desktop_launch_ready command without any pre-configured scope.","commands":{"allow":[],"deny":["desktop_launch_ready"]}},"deny-developer-run-get":{"identifier":"deny-developer-run-get","description":"Denies the developer_run_get command without any pre-configured scope.","commands":{"allow":[],"deny":["developer_run_get"]}},"deny-developer-run-start":{"identifier":"deny-developer-run-start","description":"Denies the developer_run_start command without any pre-configured scope.","commands":{"allow":[],"deny":["developer_run_start"]}},"deny-developer-run-stop":{"identifier":"deny-developer-run-stop","description":"Denies the developer_run_stop command without any pre-configured scope.","commands":{"allow":[],"deny":["developer_run_stop"]}},"deny-install-stable-update":{"identifier":"deny-install-stable-update","description":"Denies the install_stable_update command without any pre-configured scope.","commands":{"allow":[],"deny":["install_stable_update"]}},"deny-needs-local-certificate-install":{"identifier":"deny-needs-local-certificate-install","description":"Denies the needs_local_certificate_install command without any pre-configured scope.","commands":{"allow":[],"deny":["needs_local_certificate_install"]}},"deny-open-remote-window":{"identifier":"deny-open-remote-window","description":"Denies the open_remote_window command without any pre-configured scope.","commands":{"allow":[],"deny":["open_remote_window"]}},"deny-open-workspace-target":{"identifier":"deny-open-workspace-target","description":"Denies the open_workspace_target command without any pre-configured scope.","commands":{"allow":[],"deny":["open_workspace_target"]}},"deny-set-workspace-menu-enabled":{"identifier":"deny-set-workspace-menu-enabled","description":"Denies the set_workspace_menu_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_workspace_menu_enabled"]}},"deny-wake-lock-start":{"identifier":"deny-wake-lock-start","description":"Denies the wake_lock_start command without any pre-configured scope.","commands":{"allow":[],"deny":["wake_lock_start"]}},"deny-wake-lock-stop":{"identifier":"deny-wake-lock-stop","description":"Denies the wake_lock_stop command without any pre-configured scope.","commands":{"allow":[],"deny":["wake_lock_stop"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","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","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"global-shortcut":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n","permissions":[]},"permissions":{"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-register-all":{"identifier":"allow-register-all","description":"Enables the register_all command without any pre-configured scope.","commands":{"allow":["register_all"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"allow-unregister-all":{"identifier":"allow-unregister-all","description":"Enables the unregister_all command without any pre-configured scope.","commands":{"allow":["unregister_all"],"deny":[]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-register-all":{"identifier":"deny-register-all","description":"Denies the register_all command without any pre-configured scope.","commands":{"allow":[],"deny":["register_all"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}},"deny-unregister-all":{"identifier":"deny-unregister-all","description":"Denies the unregister_all command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister_all"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","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","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","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","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"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"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"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"}},"required":["path"],"type":"object"}],"definitions":{"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"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}}} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/gen/schemas/capabilities.json b/packages/tauri-app/src-tauri/gen/schemas/capabilities.json index b079316f6..551333ffd 100644 --- a/packages/tauri-app/src-tauri/gen/schemas/capabilities.json +++ b/packages/tauri-app/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant local windows access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:1420","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["local-*"],"permissions":["core:default","core:menu:default","dialog:allow-open",{"identifier":"opener:allow-open-url","allow":[{"url":"http://*"},{"url":"https://*"},{"url":"mailto:*"}]},"notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-show","core:webview:allow-set-webview-zoom","allow-cli-get-status","allow-cli-restart","allow-wake-lock-start","allow-wake-lock-stop","allow-needs-local-certificate-install","allow-open-remote-window","allow-client-state-claim-access","allow-client-state-load","allow-client-state-save","allow-client-state-commit-partitions","allow-client-state-load-partition","allow-client-state-set-restore-enabled","allow-client-state-clear","allow-client-state-renderer-flushed","allow-client-state-navigation-flushed","allow-desktop-launch-ready","allow-desktop-launch-next-folder","allow-desktop-launch-acknowledge-folder","allow-install-stable-update","allow-open-workspace-target","allow-set-workspace-menu-enabled","allow-developer-run-get","allow-developer-run-start","allow-developer-run-stop"]},"remote-window-notifications":{"identifier":"remote-window-notifications","description":"Grant remote CodeNomad windows access only to native OS notifications.","remote":{"urls":["http://*:*","https://*:*"]},"local":false,"windows":["remote-*"],"permissions":["notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify"]}} \ No newline at end of file +{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant local windows access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:1420","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["local-*"],"permissions":["core:default","core:menu:default","dialog:allow-open",{"identifier":"opener:allow-open-url","allow":[{"url":"http://*"},{"url":"https://*"},{"url":"mailto:*"}]},"notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-show","core:webview:allow-set-webview-zoom","allow-cli-get-status","allow-cli-restart","allow-wake-lock-start","allow-wake-lock-stop","allow-needs-local-certificate-install","allow-open-remote-window","allow-client-state-claim-access","allow-client-state-load","allow-client-state-save","allow-client-state-commit-partitions","allow-client-state-load-partition","allow-client-state-set-restore-enabled","allow-client-state-clear","allow-client-state-renderer-flushed","allow-client-state-navigation-flushed","allow-desktop-launch-ready","allow-desktop-launch-next-folder","allow-desktop-launch-acknowledge-folder","allow-install-stable-update","allow-open-workspace-target","allow-set-workspace-menu-enabled","allow-developer-run-get","allow-developer-run-start","allow-developer-run-stop","allow-browser-target-register","allow-browser-target-update","allow-browser-target-action","allow-browser-target-unregister","allow-browser-target-claim-open"]},"remote-window-notifications":{"identifier":"remote-window-notifications","description":"Grant remote CodeNomad windows access only to native OS notifications.","remote":{"urls":["http://*:*","https://*:*"]},"local":false,"windows":["remote-*"],"permissions":["notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify"]}} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json b/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json index 3c3752dac..dea889317 100644 --- a/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json +++ b/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json @@ -344,6 +344,36 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "Enables the browser_target_action command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-action", + "markdownDescription": "Enables the browser_target_action command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_claim_open command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-claim-open", + "markdownDescription": "Enables the browser_target_claim_open command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_register command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-register", + "markdownDescription": "Enables the browser_target_register command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_unregister command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-unregister", + "markdownDescription": "Enables the browser_target_unregister command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_update command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-update", + "markdownDescription": "Enables the browser_target_update command without any pre-configured scope." + }, { "description": "Enables the cli_get_status command without any pre-configured scope.", "type": "string", @@ -488,6 +518,36 @@ "const": "allow-wake-lock-stop", "markdownDescription": "Enables the wake_lock_stop command without any pre-configured scope." }, + { + "description": "Denies the browser_target_action command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-action", + "markdownDescription": "Denies the browser_target_action command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_claim_open command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-claim-open", + "markdownDescription": "Denies the browser_target_claim_open command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_register command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-register", + "markdownDescription": "Denies the browser_target_register command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_unregister command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-unregister", + "markdownDescription": "Denies the browser_target_unregister command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_update command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-update", + "markdownDescription": "Denies the browser_target_update command without any pre-configured scope." + }, { "description": "Denies the cli_get_status command without any pre-configured scope.", "type": "string", diff --git a/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json b/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json index 3c3752dac..dea889317 100644 --- a/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json +++ b/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json @@ -344,6 +344,36 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "Enables the browser_target_action command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-action", + "markdownDescription": "Enables the browser_target_action command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_claim_open command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-claim-open", + "markdownDescription": "Enables the browser_target_claim_open command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_register command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-register", + "markdownDescription": "Enables the browser_target_register command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_unregister command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-unregister", + "markdownDescription": "Enables the browser_target_unregister command without any pre-configured scope." + }, + { + "description": "Enables the browser_target_update command without any pre-configured scope.", + "type": "string", + "const": "allow-browser-target-update", + "markdownDescription": "Enables the browser_target_update command without any pre-configured scope." + }, { "description": "Enables the cli_get_status command without any pre-configured scope.", "type": "string", @@ -488,6 +518,36 @@ "const": "allow-wake-lock-stop", "markdownDescription": "Enables the wake_lock_stop command without any pre-configured scope." }, + { + "description": "Denies the browser_target_action command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-action", + "markdownDescription": "Denies the browser_target_action command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_claim_open command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-claim-open", + "markdownDescription": "Denies the browser_target_claim_open command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_register command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-register", + "markdownDescription": "Denies the browser_target_register command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_unregister command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-unregister", + "markdownDescription": "Denies the browser_target_unregister command without any pre-configured scope." + }, + { + "description": "Denies the browser_target_update command without any pre-configured scope.", + "type": "string", + "const": "deny-browser-target-update", + "markdownDescription": "Denies the browser_target_update command without any pre-configured scope." + }, { "description": "Denies the cli_get_status command without any pre-configured scope.", "type": "string", diff --git a/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_action.toml b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_action.toml new file mode 100644 index 000000000..d4b1e62a2 --- /dev/null +++ b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_action.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-browser-target-action" +description = "Enables the browser_target_action command without any pre-configured scope." +commands.allow = ["browser_target_action"] + +[[permission]] +identifier = "deny-browser-target-action" +description = "Denies the browser_target_action command without any pre-configured scope." +commands.deny = ["browser_target_action"] diff --git a/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_claim_open.toml b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_claim_open.toml new file mode 100644 index 000000000..bfbf7032a --- /dev/null +++ b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_claim_open.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-browser-target-claim-open" +description = "Enables the browser_target_claim_open command without any pre-configured scope." +commands.allow = ["browser_target_claim_open"] + +[[permission]] +identifier = "deny-browser-target-claim-open" +description = "Denies the browser_target_claim_open command without any pre-configured scope." +commands.deny = ["browser_target_claim_open"] diff --git a/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_register.toml b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_register.toml new file mode 100644 index 000000000..54b91fa18 --- /dev/null +++ b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_register.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-browser-target-register" +description = "Enables the browser_target_register command without any pre-configured scope." +commands.allow = ["browser_target_register"] + +[[permission]] +identifier = "deny-browser-target-register" +description = "Denies the browser_target_register command without any pre-configured scope." +commands.deny = ["browser_target_register"] diff --git a/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_unregister.toml b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_unregister.toml new file mode 100644 index 000000000..a815f9365 --- /dev/null +++ b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_unregister.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-browser-target-unregister" +description = "Enables the browser_target_unregister command without any pre-configured scope." +commands.allow = ["browser_target_unregister"] + +[[permission]] +identifier = "deny-browser-target-unregister" +description = "Denies the browser_target_unregister command without any pre-configured scope." +commands.deny = ["browser_target_unregister"] diff --git a/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_update.toml b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_update.toml new file mode 100644 index 000000000..578200b5d --- /dev/null +++ b/packages/tauri-app/src-tauri/permissions/autogenerated/browser_target_update.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-browser-target-update" +description = "Enables the browser_target_update command without any pre-configured scope." +commands.allow = ["browser_target_update"] + +[[permission]] +identifier = "deny-browser-target-update" +description = "Denies the browser_target_update command without any pre-configured scope." +commands.deny = ["browser_target_update"] diff --git a/packages/tauri-app/src-tauri/src/browser_controller.rs b/packages/tauri-app/src-tauri/src/browser_controller.rs new file mode 100644 index 000000000..ff03ea9ac --- /dev/null +++ b/packages/tauri-app/src-tauri/src/browser_controller.rs @@ -0,0 +1,1324 @@ +use serde::Deserialize; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use tauri::webview::PageLoadEvent; +use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize, WebviewWindow}; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BrowserTargetRegistration { + session_id: String, + registration_id: String, + url: String, + bounds: BrowserTargetBounds, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +pub(crate) struct BrowserTargetBounds { + x: i32, + y: i32, + width: u32, + height: u32, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BrowserTargetUpdate { + registration_id: String, + bounds: Option, + visible: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BrowserTargetAction { + registration_id: String, + action: String, + url: Option, +} + +#[derive(Clone)] +struct Registration { + session_id: String, + registration_id: String, + window_label: String, + webview_label: String, + generation: u64, + visible: bool, +} + +#[derive(Clone)] +struct PageLoadExpectation { + sequence: u64, + url: String, + navigation_id: Option, + result: Option>, +} + +#[derive(Default)] +struct Inner { + registrations: HashMap, + refs: HashMap>, + navigation_versions: HashMap, + page_load_sequence: u64, + page_load_expectations: HashMap, + open_claims: HashMap>, +} + +#[derive(Clone)] +pub(crate) struct BrowserController { + inner: Arc<(Mutex, Condvar)>, + profile: Arc, + registration_sequence: Arc, +} + +impl BrowserController { + pub(crate) fn new(profile: std::path::PathBuf) -> Self { + Self { + inner: Arc::new((Mutex::new(Inner::default()), Condvar::new())), + profile: Arc::new(profile), + registration_sequence: Arc::new(AtomicU64::new(1)), + } + } + + #[cfg(windows)] + pub(crate) fn register( + &self, + window: &WebviewWindow, + input: BrowserTargetRegistration, + ) -> Result<(), String> { + use tauri::webview::{NewWindowResponse, WebviewBuilder}; + use tauri::WebviewUrl; + + validate_id(&input.session_id, 256)?; + validate_id(&input.registration_id, 128)?; + let url = allowed_url(&input.url)?; + let blank = "about:blank" + .parse() + .map_err(|error| format!("failed to prepare browser preview: {error}"))?; + validate_bounds(input.bounds)?; + let label = format!("browser-{}", input.registration_id); + let registration_id = input.registration_id.clone(); + let generation = self.registration_sequence.fetch_add(1, Ordering::Relaxed); + let navigation_controller = self.clone(); + let navigation_registration_id = registration_id.clone(); + let policy_controller = self.clone(); + let policy_registration_id = registration_id.clone(); + let profile = self + .profile + .join(format!("{:x}", Sha256::digest(input.session_id.as_bytes()))); + let builder = WebviewBuilder::new(label.clone(), WebviewUrl::External(blank)) + .data_directory(profile) + .on_navigation(move |url| { + let allowed = url.as_str() == "about:blank" || is_allowed_url(url); + if allowed { + policy_controller + .clear_refs_for_generation(&policy_registration_id, generation); + } + allowed + }) + .on_new_window(|_, _| NewWindowResponse::Deny) + .on_download(|_, _| false) + .on_page_load(move |webview, payload| { + if payload.event() == PageLoadEvent::Started + && payload.url().as_str() != "about:blank" + { + navigation_controller + .clear_refs_for_generation(&navigation_registration_id, generation); + let _ = webview.window().emit( + "browser-target:navigated", + json!({ "registrationId": registration_id, "url": payload.url().as_str() }), + ); + } + }); + let webview = window + .as_ref() + .window() + .add_child( + builder, + PhysicalPosition::new(input.bounds.x, input.bounds.y), + PhysicalSize::new(input.bounds.width, input.bounds.height), + ) + .map_err(|error| format!("failed to create browser preview: {error}"))?; + if let Err(error) = install_webview2_handlers( + &webview, + self.clone(), + input.registration_id.clone(), + window.label().to_string(), + generation, + ) { + let _ = webview.close(); + return Err(error); + } + if let Err(error) = webview.show() { + let _ = webview.close(); + return Err(error.to_string()); + } + + let registration = Registration { + session_id: input.session_id, + registration_id: input.registration_id.clone(), + window_label: window.label().to_string(), + webview_label: label, + generation, + visible: true, + }; + let (inner, ready) = &*self.inner; + let mut inner = inner.lock().map_err(|error| error.to_string())?; + let previous = inner + .registrations + .insert(input.registration_id.clone(), registration); + inner.refs.remove(&input.registration_id); + inner + .navigation_versions + .insert(input.registration_id.clone(), 0); + inner.page_load_sequence = inner.page_load_sequence.wrapping_add(1); + let load_sequence = inner.page_load_sequence; + inner.page_load_expectations.insert( + input.registration_id.clone(), + PageLoadExpectation { + sequence: load_sequence, + url: url.to_string(), + navigation_id: None, + result: None, + }, + ); + drop(inner); + if let Some(previous) = previous { + if let Some(previous_webview) = window.app_handle().get_webview(&previous.webview_label) + { + let _ = previous_webview.close(); + } + } + ready.notify_all(); + if let Err(error) = webview.navigate(url) { + let _ = self.unregister(window.app_handle(), window.label(), &input.registration_id); + return Err(error.to_string()); + } + Ok(()) + } + + #[cfg(not(windows))] + pub(crate) fn register( + &self, + _window: &WebviewWindow, + _input: BrowserTargetRegistration, + ) -> Result<(), String> { + Err(unsupported()) + } + + pub(crate) fn update( + &self, + app: &AppHandle, + window_label: &str, + input: BrowserTargetUpdate, + ) -> Result<(), String> { + let registration = self.owned_registration(window_label, &input.registration_id)?; + let webview = app + .get_webview(®istration.webview_label) + .ok_or_else(|| "Browser preview is no longer available".to_string())?; + let visible = input.visible.unwrap_or(true); + if visible { + let bounds = input + .bounds + .ok_or_else(|| "Visible browser target bounds are required".to_string())?; + validate_bounds(bounds)?; + webview + .set_position(PhysicalPosition::new(bounds.x, bounds.y)) + .and_then(|()| webview.set_size(PhysicalSize::new(bounds.width, bounds.height))) + .and_then(|()| webview.show()) + .map_err(|error| error.to_string())?; + } else { + webview.hide().map_err(|error| error.to_string())?; + } + let mut inner = self.inner.0.lock().map_err(|error| error.to_string())?; + if let Some(current) = inner.registrations.get_mut(&input.registration_id) { + if current.generation == registration.generation { + current.visible = visible; + } + } + Ok(()) + } + + #[cfg(windows)] + pub(crate) fn action( + &self, + app: &AppHandle, + window_label: &str, + input: BrowserTargetAction, + ) -> Result<(), String> { + let registration = self.owned_registration(window_label, &input.registration_id)?; + let webview = app + .get_webview(®istration.webview_label) + .ok_or_else(|| "Browser preview is no longer available".to_string())?; + match input.action.as_str() { + "back" => webview + .eval("history.back()") + .map_err(|error| error.to_string()), + "reload" => webview.reload().map_err(|error| error.to_string()), + "navigate" => webview + .navigate(allowed_url(input.url.as_deref().ok_or_else(|| { + "Browser navigation URL is required".to_string() + })?)?) + .map_err(|error| error.to_string()), + _ => Err("Unsupported browser target action".to_string()), + } + } + + #[cfg(not(windows))] + pub(crate) fn action( + &self, + _app: &AppHandle, + _window_label: &str, + _input: BrowserTargetAction, + ) -> Result<(), String> { + Err(unsupported()) + } + + pub(crate) fn unregister( + &self, + app: &AppHandle, + window_label: &str, + registration_id: &str, + ) -> Result<(), String> { + let (inner, ready) = &*self.inner; + let mut inner = inner.lock().map_err(|error| error.to_string())?; + let Some(registration) = inner.registrations.get(registration_id) else { + return Ok(()); + }; + if registration.window_label != window_label { + return Err("Browser target belongs to another window".to_string()); + } + let registration = inner.registrations.remove(registration_id).unwrap(); + inner.refs.remove(registration_id); + inner.navigation_versions.remove(registration_id); + inner.page_load_expectations.remove(registration_id); + drop(inner); + ready.notify_all(); + if let Some(webview) = app.get_webview(®istration.webview_label) { + webview.close().map_err(|error| error.to_string())?; + } + Ok(()) + } + + pub(crate) fn claim_open(&self, window_label: &str, request_id: &str) -> bool { + let Ok(mut inner) = self.inner.0.lock() else { + return false; + }; + let Some(claim) = inner.open_claims.get_mut(request_id) else { + return false; + }; + if claim.as_deref().is_some_and(|owner| owner != window_label) { + return false; + } + *claim = Some(window_label.to_string()); + true + } + + pub(crate) fn remove_window(&self, app: &AppHandle, window_label: &str) { + let labels = if let Ok(mut inner) = self.inner.0.lock() { + let ids = inner + .registrations + .values() + .filter(|registration| registration.window_label == window_label) + .map(|registration| registration.registration_id.clone()) + .collect::>(); + ids.into_iter() + .filter_map(|id| { + inner.refs.remove(&id); + inner.navigation_versions.remove(&id); + inner.page_load_expectations.remove(&id); + inner + .registrations + .remove(&id) + .map(|item| item.webview_label) + }) + .collect::>() + } else { + Vec::new() + }; + for label in labels { + if let Some(webview) = app.get_webview(&label) { + let _ = webview.close(); + } + } + self.inner.1.notify_all(); + } + + pub(crate) fn handle_native( + &self, + app: &AppHandle, + method: &str, + params: Option, + deadline_ms: u64, + ) -> Result { + #[cfg(not(windows))] + { + let _ = (app, method, params, deadline_ms); + return Err(unsupported()); + } + #[cfg(windows)] + { + let input = params.ok_or_else(|| "Invalid browser request".to_string())?; + let deadline = request_deadline(deadline_ms)?; + let session_id = input + .get("sessionID") + .and_then(Value::as_str) + .ok_or_else(|| "Invalid browser request".to_string())?; + match method { + "browser.probe" => { + self.resolve(app, session_id)?; + Ok(json!({ "available": true })) + } + "browser.execute" => self.execute( + app, + session_id, + input + .get("command") + .cloned() + .ok_or_else(|| "Invalid browser request".to_string())?, + deadline, + ), + _ => Err(format!("Unsupported native method: {method}")), + } + } + } + + #[cfg(windows)] + fn execute( + &self, + app: &AppHandle, + session_id: &str, + command: Value, + deadline: Instant, + ) -> Result { + let action = command + .get("action") + .and_then(Value::as_str) + .ok_or_else(|| "Invalid browser command".to_string())?; + if action == "open" { + let url = command_url(&command)?; + return self.open(app, session_id, url, deadline); + } + let registration = self.resolve(app, session_id)?; + let webview = app + .get_webview(®istration.webview_label) + .ok_or_else(|| "Browser preview is no longer available".to_string())?; + match action { + "navigate" => { + let url = allowed_url(command_url(&command)?)?; + self.navigate_and_wait(®istration, &webview, &url, deadline)?; + Ok(json!({ "url": webview.url().map_err(|error| error.to_string())? })) + } + "snapshot" => self.snapshot(®istration, &webview, deadline), + "click" => { + let target = command_ref(&command)?; + self.click(®istration, &webview, target, deadline)?; + Ok( + json!({ "clicked": target, "url": webview.url().map_err(|error| error.to_string())? }), + ) + } + "type" => { + let target = command_ref(&command)?; + self.click(®istration, &webview, target, deadline)?; + if command.get("clear").and_then(Value::as_bool) != Some(false) { + clear_focused_field(&webview, deadline)?; + } + let text = command + .get("text") + .and_then(Value::as_str) + .ok_or_else(|| "Browser text is required".to_string())?; + cdp( + &webview, + "Input.insertText", + json!({ "text": text }), + deadline, + )?; + Ok( + json!({ "typed": target, "url": webview.url().map_err(|error| error.to_string())? }), + ) + } + "screenshot" => { + let result = cdp( + &webview, + "Page.captureScreenshot", + json!({ "format": "png", "fromSurface": true }), + deadline, + )?; + let data = result + .get("data") + .and_then(Value::as_str) + .filter(|data| data.len() <= 16 * 1024 * 1024) + .ok_or_else(|| "Browser screenshot is unavailable or too large".to_string())?; + Ok( + json!({ "image": { "mime": "image/png", "data": data }, "url": webview.url().map_err(|error| error.to_string())? }), + ) + } + _ => Err("Invalid browser command".to_string()), + } + } + + #[cfg(windows)] + fn open( + &self, + app: &AppHandle, + session_id: &str, + raw_url: &str, + deadline: Instant, + ) -> Result { + let url = allowed_url(raw_url)?; + match self.resolve(app, session_id) { + Ok(registration) => { + let webview = app + .get_webview(®istration.webview_label) + .ok_or_else(|| "Browser preview is no longer available".to_string())?; + self.navigate_and_wait(®istration, &webview, &url, deadline)?; + return Ok(json!({ "url": webview.url().map_err(|error| error.to_string())? })); + } + Err(error) if error == no_browser_target_error() => {} + Err(error) => return Err(error), + } + + let request_id = uuid::Uuid::new_v4().to_string(); + { + let mut inner = self.inner.0.lock().map_err(|error| error.to_string())?; + inner.open_claims.insert(request_id.clone(), None); + } + for record in app.state::().records() { + if let Some(window) = app.get_webview_window(&record.label) { + let _ = window.emit( + "browser-target:open", + json!({ "sessionID": session_id, "url": raw_url, "requestID": request_id }), + ); + } + } + let result = (|| -> Result { + let registration_deadline = deadline.min(Instant::now() + Duration::from_secs(10)); + let (inner, ready) = &*self.inner; + let mut guard = inner.lock().map_err(|error| error.to_string())?; + loop { + drop(guard); + match self.resolve(app, session_id) { + Ok(registration) => { + let webview = app + .get_webview(®istration.webview_label) + .ok_or_else(|| "Browser preview is no longer available".to_string())?; + let sequence = self + .page_load_expectation(®istration.registration_id, url.as_str())?; + self.wait_for_page_load(®istration.registration_id, sequence, deadline)?; + return Ok( + json!({ "url": webview.url().map_err(|error| error.to_string())? }), + ); + } + Err(error) if error == no_browser_target_error() => {} + Err(error) => return Err(error), + } + let remaining = registration_deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err( + "Timed out waiting for CodeNomad to open the browser preview".to_string(), + ); + } + guard = ready + .wait_timeout( + inner.lock().map_err(|error| error.to_string())?, + remaining.min(Duration::from_millis(50)), + ) + .map_err(|error| error.to_string())? + .0; + } + })(); + if let Ok(mut inner) = self.inner.0.lock() { + inner.open_claims.remove(&request_id); + } + result + } + + #[cfg(windows)] + fn resolve(&self, app: &AppHandle, session_id: &str) -> Result { + let registrations = self + .inner + .0 + .lock() + .map_err(|error| error.to_string())? + .registrations + .values() + .filter(|registration| registration.session_id == session_id && registration.visible) + .cloned() + .collect::>(); + let matches = registrations + .into_iter() + .filter(|registration| { + app.get_window(®istration.window_label) + .is_some_and(|window| { + window.is_visible().unwrap_or(false) + && !window.is_minimized().unwrap_or(true) + }) + }) + .collect::>(); + match matches.as_slice() { + [] => Err(no_browser_target_error()), + [registration] => Ok(registration.clone()), + _ => Err("Multiple visible browser targets exist for this session".to_string()), + } + } + + pub(crate) fn navigation_allowed(&self, webview_label: &str, url: &tauri::Url) -> Option { + if webview_label + .strip_prefix("browser-") + .is_some_and(|id| validate_id(id, 128).is_ok()) + { + return Some(is_allowed_url(url)); + } + self.inner.0.lock().ok().and_then(|inner| { + inner + .registrations + .values() + .any(|registration| registration.webview_label == webview_label) + .then(|| is_allowed_url(url)) + }) + } + + #[cfg(windows)] + fn snapshot( + &self, + registration: &Registration, + webview: &tauri::Webview, + deadline: Instant, + ) -> Result { + let navigation_version = self + .inner + .0 + .lock() + .map_err(|error| error.to_string())? + .navigation_versions + .get(®istration.registration_id) + .copied(); + cdp(webview, "Accessibility.enable", json!({}), deadline)?; + let result = cdp(webview, "Accessibility.getFullAXTree", json!({}), deadline)?; + let mut refs = HashMap::new(); + let mut lines = Vec::new(); + let mut length = 0; + for node in result + .get("nodes") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + if node.get("ignored").and_then(Value::as_bool) == Some(true) { + continue; + } + let Some(backend_id) = node.get("backendDOMNodeId").and_then(Value::as_i64) else { + continue; + }; + let role = ax_string(node.get("role")); + let name = ax_string(node.get("name")); + if role.is_empty() + || (name.is_empty() + && ["generic", "none", "StaticText", "InlineTextBox"].contains(&role.as_str())) + { + continue; + } + let reference = format!("e{}", refs.len() + 1); + refs.insert(reference.clone(), backend_id); + let value = ax_string(node.get("value")); + let states = node + .get("properties") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|property| { + let key = property.get("name")?.as_str()?; + if !["checked", "disabled", "expanded", "focused", "selected"].contains(&key) { + return None; + } + Some(format!("{key}={}", property.pointer("/value/value")?)) + }) + .collect::>(); + let line = format!( + "[{reference}] {role}{}{}{}", + (!name.is_empty()) + .then(|| format!(" {name:?}")) + .unwrap_or_default(), + (!value.is_empty()) + .then(|| format!(" value={value:?}")) + .unwrap_or_default(), + (!states.is_empty()) + .then(|| format!(" {}", states.join(" "))) + .unwrap_or_default(), + ); + if lines.len() >= 750 || length + line.len() > 64 * 1024 { + break; + } + length += line.len() + 1; + lines.push(line); + } + let mut inner = self.inner.0.lock().map_err(|error| error.to_string())?; + if inner + .navigation_versions + .get(®istration.registration_id) + .copied() + == navigation_version + { + inner + .refs + .insert(registration.registration_id.clone(), refs); + } + Ok(json!({ + "url": webview.url().map_err(|error| error.to_string())?, + "snapshot": if lines.is_empty() { "No accessible elements found".to_string() } else { lines.join("\n") }, + })) + } + + #[cfg(windows)] + fn navigate_and_wait( + &self, + registration: &Registration, + webview: &tauri::Webview, + url: &tauri::Url, + deadline: Instant, + ) -> Result<(), String> { + self.clear_refs(®istration.registration_id); + let sequence = self.prepare_page_load(®istration.registration_id, url.as_str())?; + let result = cdp( + webview, + "Page.navigate", + json!({ "url": url.as_str() }), + deadline, + )?; + if let Some(error) = result + .get("errorText") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + { + return Err(format!("Browser navigation failed: {error}")); + } + self.wait_for_page_load(®istration.registration_id, sequence, deadline) + } + + #[cfg(windows)] + fn click( + &self, + registration: &Registration, + webview: &tauri::Webview, + target: &str, + deadline: Instant, + ) -> Result<(), String> { + let (backend_node_id, navigation_version) = { + let inner = self.inner.0.lock().map_err(|error| error.to_string())?; + let backend_node_id = inner + .refs + .get(®istration.registration_id) + .and_then(|refs| refs.get(target)) + .copied() + .ok_or_else(|| format!("Unknown browser ref {target}; take a new snapshot"))?; + ( + backend_node_id, + inner + .navigation_versions + .get(®istration.registration_id) + .copied(), + ) + }; + cdp( + webview, + "DOM.scrollIntoViewIfNeeded", + json!({ "backendNodeId": backend_node_id }), + deadline, + )?; + let result = cdp( + webview, + "DOM.getBoxModel", + json!({ "backendNodeId": backend_node_id }), + deadline, + )?; + let box_model = result + .pointer("/model/content") + .and_then(Value::as_array) + .filter(|values| values.len() == 8) + .ok_or_else(|| format!("Browser ref {target} is not visible"))?; + let x = [0, 2, 4, 6] + .into_iter() + .filter_map(|index| box_model[index].as_f64()) + .sum::() + / 4.0; + let y = [1, 3, 5, 7] + .into_iter() + .filter_map(|index| box_model[index].as_f64()) + .sum::() + / 4.0; + for (kind, extra) in [ + ("mouseMoved", json!({})), + ("mousePressed", json!({ "button": "left", "clickCount": 1 })), + ( + "mouseReleased", + json!({ "button": "left", "clickCount": 1 }), + ), + ] { + if self.page_navigation_version(®istration.registration_id) != navigation_version { + return Err(format!("Unknown browser ref {target}; take a new snapshot")); + } + let mut params = json!({ "type": kind, "x": x, "y": y }); + params + .as_object_mut() + .unwrap() + .extend(extra.as_object().unwrap().clone()); + cdp(webview, "Input.dispatchMouseEvent", params, deadline)?; + } + Ok(()) + } + + fn page_navigation_version(&self, registration_id: &str) -> Option { + self.inner + .0 + .lock() + .ok()? + .navigation_versions + .get(registration_id) + .copied() + } + + fn owned_registration(&self, window_label: &str, id: &str) -> Result { + let inner = self.inner.0.lock().map_err(|error| error.to_string())?; + let registration = inner + .registrations + .get(id) + .ok_or_else(|| "Browser target is not registered".to_string())?; + if registration.window_label != window_label { + return Err("Browser target belongs to another window".to_string()); + } + Ok(registration.clone()) + } + + fn clear_refs(&self, registration_id: &str) { + if let Ok(mut inner) = self.inner.0.lock() { + inner.refs.remove(registration_id); + if let Some(version) = inner.navigation_versions.get_mut(registration_id) { + *version = version.wrapping_add(1); + } + } + } + + fn clear_refs_for_generation(&self, registration_id: &str, generation: u64) { + if let Ok(mut inner) = self.inner.0.lock() { + if inner + .registrations + .get(registration_id) + .map(|item| item.generation) + != Some(generation) + { + return; + } + inner.refs.remove(registration_id); + if let Some(version) = inner.navigation_versions.get_mut(registration_id) { + *version = version.wrapping_add(1); + } + } + self.inner.1.notify_all(); + } + + fn prepare_page_load(&self, registration_id: &str, url: &str) -> Result { + let mut inner = self.inner.0.lock().map_err(|error| error.to_string())?; + if !inner.registrations.contains_key(registration_id) { + return Err("Browser preview is no longer available".to_string()); + } + inner.page_load_sequence = inner.page_load_sequence.wrapping_add(1); + let sequence = inner.page_load_sequence; + inner.page_load_expectations.insert( + registration_id.to_string(), + PageLoadExpectation { + sequence, + url: url.to_string(), + navigation_id: None, + result: None, + }, + ); + Ok(sequence) + } + + fn page_load_expectation(&self, registration_id: &str, url: &str) -> Result { + let inner = self.inner.0.lock().map_err(|error| error.to_string())?; + inner + .page_load_expectations + .get(registration_id) + .filter(|expectation| expectation.url == url) + .map(|expectation| expectation.sequence) + .ok_or_else(|| "Browser preview navigation is no longer current".to_string()) + } + + fn mark_page_started( + &self, + registration_id: &str, + generation: u64, + navigation_id: u64, + url: String, + ) { + if let Ok(mut inner) = self.inner.0.lock() { + if inner + .registrations + .get(registration_id) + .map(|item| item.generation) + == Some(generation) + { + if let Some(expectation) = inner.page_load_expectations.get_mut(registration_id) { + if expectation.url == url && expectation.navigation_id.is_none() { + expectation.navigation_id = Some(navigation_id); + } + } + } + } + self.inner.1.notify_all(); + } + + fn mark_page_finished( + &self, + registration_id: &str, + generation: u64, + navigation_id: u64, + error: Option, + ) { + if let Ok(mut inner) = self.inner.0.lock() { + if inner + .registrations + .get(registration_id) + .map(|item| item.generation) + != Some(generation) + { + return; + } + if let Some(expectation) = inner.page_load_expectations.get_mut(registration_id) { + if expectation.navigation_id == Some(navigation_id) { + expectation.result = Some(error.map_or(Ok(()), Err)); + } + } + } + self.inner.1.notify_all(); + } + + fn wait_for_page_load( + &self, + registration_id: &str, + sequence: u64, + request_deadline: Instant, + ) -> Result<(), String> { + let deadline = request_deadline.min(Instant::now() + Duration::from_secs(15)); + let (inner, ready) = &*self.inner; + let mut guard = inner.lock().map_err(|error| error.to_string())?; + loop { + let expectation = guard + .page_load_expectations + .get(registration_id) + .filter(|expectation| expectation.sequence == sequence) + .ok_or_else(|| "Browser preview navigation is no longer current".to_string())?; + if let Some(result) = &expectation.result { + return result.clone(); + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err("Timed out waiting for the browser page to load".to_string()); + } + guard = ready + .wait_timeout(guard, remaining) + .map_err(|error| error.to_string())? + .0; + } + } +} + +fn validate_id(value: &str, max: usize) -> Result<(), String> { + if value.is_empty() + || value.len() > max + || !value + .chars() + .all(|char| char.is_ascii_alphanumeric() || "-_".contains(char)) + { + return Err("Invalid browser target registration".to_string()); + } + Ok(()) +} + +fn request_deadline(deadline_ms: u64) -> Result { + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| "Native request expired before execution".to_string())? + .as_millis() as u64; + if deadline_ms <= now_ms { + return Err("Native request expired before execution".to_string()); + } + Instant::now() + .checked_add(Duration::from_millis(deadline_ms - now_ms)) + .ok_or_else(|| "Invalid native request deadline".to_string()) +} + +fn validate_bounds(bounds: BrowserTargetBounds) -> Result<(), String> { + if bounds.width == 0 || bounds.height == 0 || bounds.width > 16_384 || bounds.height > 16_384 { + return Err("Invalid browser target bounds".to_string()); + } + Ok(()) +} + +fn allowed_url(raw: &str) -> Result { + let url = tauri::Url::parse(raw).map_err(|_| navigation_error())?; + is_allowed_url(&url) + .then_some(url) + .ok_or_else(navigation_error) +} + +fn is_allowed_url(url: &tauri::Url) -> bool { + matches!(url.scheme(), "http" | "https") + && url.host().is_some() + && url.username().is_empty() + && url.password().is_none() +} + +fn navigation_error() -> String { + "Browser navigation requires a credential-free HTTP(S) URL".to_string() +} + +fn no_browser_target_error() -> String { + "No visible local browser target for this session".to_string() +} + +#[cfg(not(windows))] +fn unsupported() -> String { + "Native browser control is only supported by the Tauri Windows runtime".to_string() +} + +#[cfg(windows)] +fn command_url(command: &Value) -> Result<&str, String> { + command + .get("url") + .and_then(Value::as_str) + .ok_or_else(|| "Browser navigation URL is required".to_string()) +} + +#[cfg(windows)] +fn command_ref(command: &Value) -> Result<&str, String> { + command + .get("ref") + .and_then(Value::as_str) + .filter(|value| !value.is_empty() && value.len() <= 32) + .ok_or_else(|| "Browser ref is required".to_string()) +} + +#[cfg(windows)] +fn ax_string(value: Option<&Value>) -> String { + value + .and_then(|value| value.get("value")) + .and_then(Value::as_str) + .map(|value| value.split_whitespace().collect::>().join(" ")) + .unwrap_or_default() + .chars() + .take(500) + .collect() +} + +#[cfg(windows)] +fn clear_focused_field(webview: &tauri::Webview, deadline: Instant) -> Result<(), String> { + for params in [ + json!({ "type": "keyDown", "key": "Control", "code": "ControlLeft", "modifiers": 2 }), + json!({ "type": "keyDown", "key": "a", "code": "KeyA", "modifiers": 2 }), + json!({ "type": "keyUp", "key": "a", "code": "KeyA", "modifiers": 2 }), + json!({ "type": "keyUp", "key": "Control", "code": "ControlLeft" }), + ] { + cdp(webview, "Input.dispatchKeyEvent", params, deadline)?; + } + Ok(()) +} + +#[cfg(windows)] +fn install_webview2_handlers( + webview: &tauri::Webview, + controller: BrowserController, + registration_id: String, + window_label: String, + generation: u64, +) -> Result<(), String> { + use std::sync::mpsc; + use webview2_com::{ + CoTaskMemPWSTR, Microsoft::Web::WebView2::Win32::COREWEBVIEW2_PERMISSION_STATE_DENY, + NavigationCompletedEventHandler, NavigationStartingEventHandler, + PermissionRequestedEventHandler, SourceChangedEventHandler, + }; + + let app = webview.app_handle().clone(); + let (sender, receiver) = mpsc::sync_channel(1); + webview + .with_webview(move |platform| { + let result = (|| { + let core = unsafe { platform.controller().CoreWebView2() }?; + let permission = PermissionRequestedEventHandler::create(Box::new(|_, args| { + if let Some(args) = args { + unsafe { args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)? }; + } + Ok(()) + })); + let mut permission_token = 0; + unsafe { core.add_PermissionRequested(&permission, &mut permission_token)? }; + + let started_controller = controller.clone(); + let started_registration_id = registration_id.clone(); + let started = NavigationStartingEventHandler::create(Box::new(move |_, args| { + let Some(args) = args else { + return Ok(()); + }; + let mut navigation_id = 0; + unsafe { args.NavigationId(&mut navigation_id)? }; + let mut raw = Default::default(); + unsafe { args.Uri(&mut raw)? }; + started_controller.mark_page_started( + &started_registration_id, + generation, + navigation_id, + CoTaskMemPWSTR::from(raw).to_string(), + ); + Ok(()) + })); + let mut started_token = 0; + unsafe { core.add_NavigationStarting(&started, &mut started_token)? }; + + let completed_controller = controller.clone(); + let completed_registration_id = registration_id.clone(); + let completed = + NavigationCompletedEventHandler::create(Box::new(move |_, args| { + let Some(args) = args else { + return Ok(()); + }; + let mut navigation_id = 0; + unsafe { args.NavigationId(&mut navigation_id)? }; + let mut succeeded = Default::default(); + unsafe { args.IsSuccess(&mut succeeded)? }; + let error = if succeeded.as_bool() { + None + } else { + let mut status = Default::default(); + unsafe { args.WebErrorStatus(&mut status)? }; + Some(format!("Browser navigation failed: {status:?}")) + }; + completed_controller.mark_page_finished( + &completed_registration_id, + generation, + navigation_id, + error, + ); + Ok(()) + })); + let mut completed_token = 0; + unsafe { core.add_NavigationCompleted(&completed, &mut completed_token)? }; + + let source = SourceChangedEventHandler::create(Box::new(move |core, _| { + let Some(core) = core else { + return Ok(()); + }; + let mut raw = Default::default(); + unsafe { core.Source(&mut raw)? }; + let url = CoTaskMemPWSTR::from(raw).to_string(); + controller.clear_refs_for_generation(®istration_id, generation); + if let Some(window) = app.get_webview_window(&window_label) { + let _ = window.emit( + "browser-target:navigated", + json!({ "registrationId": registration_id, "url": url }), + ); + } + Ok(()) + })); + let mut source_token = 0; + unsafe { core.add_SourceChanged(&source, &mut source_token)? }; + Ok::<(), webview2_com::Error>(()) + })(); + let _ = sender.send(result.map_err(|error| error.to_string())); + }) + .map_err(|error| error.to_string())?; + receiver + .recv_timeout(Duration::from_secs(5)) + .map_err(|_| "Timed out configuring WebView2 browser security".to_string())? +} + +#[cfg(windows)] +fn cdp( + webview: &tauri::Webview, + method: &str, + params: Value, + deadline: Instant, +) -> Result { + use std::sync::mpsc; + use webview2_com::{CallDevToolsProtocolMethodCompletedHandler, CoTaskMemPWSTR}; + + let (sender, receiver) = mpsc::sync_channel(1); + let method = method.to_string(); + let params = params.to_string(); + webview + .with_webview(move |platform| { + let core = match unsafe { platform.controller().CoreWebView2() } { + Ok(core) => core, + Err(error) => { + let _ = sender.send(Err(error.to_string())); + return; + } + }; + let callback_sender = sender.clone(); + let handler = CallDevToolsProtocolMethodCompletedHandler::create(Box::new( + move |error, result| { + let _ = callback_sender + .send(error.map(|()| result).map_err(|error| error.to_string())); + Ok(()) + }, + )); + let method = CoTaskMemPWSTR::from(method.as_str()); + let params = CoTaskMemPWSTR::from(params.as_str()); + if let Err(error) = unsafe { + core.CallDevToolsProtocolMethod( + *method.as_ref().as_pcwstr(), + *params.as_ref().as_pcwstr(), + &handler, + ) + } { + let _ = sender.send(Err(error.to_string())); + } + }) + .map_err(|error| error.to_string())?; + let timeout = deadline + .saturating_duration_since(Instant::now()) + .min(Duration::from_secs(20)); + if timeout.is_zero() { + return Err("Native request expired before execution".to_string()); + } + let result = receiver + .recv_timeout(timeout) + .map_err(|_| "WebView2 browser command timed out".to_string())??; + serde_json::from_str(&result).map_err(|error| format!("Invalid WebView2 response: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_unsafe_browser_urls_and_bounds() { + assert!(allowed_url("https://example.com/path").is_ok()); + assert!(allowed_url("http://localhost:3000").is_ok()); + assert!(allowed_url("http://127.0.0.1:3000").is_ok()); + for url in ["file:///tmp/a", "https://user:pass@example.com"] { + assert!(allowed_url(url).is_err(), "accepted {url}"); + } + assert!(validate_bounds(BrowserTargetBounds { + x: 0, + y: 0, + width: 1, + height: 1 + }) + .is_ok()); + assert!(validate_bounds(BrowserTargetBounds { + x: 0, + y: 0, + width: 0, + height: 1 + }) + .is_err()); + } + + #[test] + fn browser_open_claims_have_one_window_owner() { + let controller = BrowserController::new(std::path::PathBuf::new()); + controller + .inner + .0 + .lock() + .unwrap() + .open_claims + .insert("request".to_string(), None); + + assert!(controller.claim_open("window-1", "request")); + assert!(controller.claim_open("window-1", "request")); + assert!(!controller.claim_open("window-2", "request")); + assert!(!controller.claim_open("window-1", "missing")); + } + + #[test] + fn failed_navigation_wakes_waiters_with_the_webview_error() { + let controller = BrowserController::new(std::path::PathBuf::new()); + let registration = Registration { + session_id: "session".to_string(), + registration_id: "registration".to_string(), + window_label: "window".to_string(), + webview_label: "webview".to_string(), + generation: 1, + visible: true, + }; + controller + .inner + .0 + .lock() + .unwrap() + .registrations + .insert(registration.registration_id.clone(), registration); + let sequence = controller + .prepare_page_load("registration", "https://example.com/") + .unwrap(); + controller.mark_page_started("registration", 1, 7, "https://example.com/".to_string()); + controller.mark_page_finished( + "registration", + 1, + 7, + Some("Browser navigation failed: ConnectionAborted".to_string()), + ); + assert_eq!( + controller + .wait_for_page_load( + "registration", + sequence, + Instant::now() + Duration::from_secs(1), + ) + .unwrap_err(), + "Browser navigation failed: ConnectionAborted" + ); + } + + #[test] + fn stale_navigation_completion_cannot_finish_the_latest_navigation() { + let controller = BrowserController::new(std::path::PathBuf::new()); + let registration = Registration { + session_id: "session".to_string(), + registration_id: "registration".to_string(), + window_label: "window".to_string(), + webview_label: "webview".to_string(), + generation: 1, + visible: true, + }; + controller + .inner + .0 + .lock() + .unwrap() + .registrations + .insert(registration.registration_id.clone(), registration); + let sequence = controller + .prepare_page_load("registration", "https://example.com/new") + .unwrap(); + controller.mark_page_started("registration", 1, 7, "about:blank".to_string()); + controller.mark_page_started("registration", 1, 8, "https://example.com/new".to_string()); + controller.mark_page_finished("registration", 1, 7, None); + assert!(controller + .inner + .0 + .lock() + .unwrap() + .page_load_expectations + .get("registration") + .unwrap() + .result + .is_none()); + controller.mark_page_finished("registration", 1, 8, None); + controller + .wait_for_page_load( + "registration", + sequence, + Instant::now() + Duration::from_secs(1), + ) + .unwrap(); + } +} diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index f6199b996..7ad980eab 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod browser_controller; #[allow(dead_code)] mod cert_manager; mod cli_manager; @@ -61,6 +62,7 @@ const REMOTE_WINDOW_CONTEXT_SCRIPT: &str = pub struct AppState { pub manager: CliProcessManager, pub(crate) developer_run_manager: developer_run::DeveloperRunManager, + pub(crate) browser_controller: browser_controller::BrowserController, pub wake_lock: Mutex, remote_navigation: Mutex>, remote_navigation_generation: AtomicU64, @@ -204,6 +206,17 @@ pub(crate) fn require_local_app_window( } } +fn require_local_app_webview(webview: &tauri::Webview, state: &AppState) -> Result { + identity::local_window_id(webview.label())?; + let current = webview.url().map_err(|error| error.to_string())?; + let status = state.manager.status(); + if is_allowed_local_origin(¤t, status.url.as_deref()) { + Ok(current) + } else { + Err("Native application commands require a trusted local renderer origin".into()) + } +} + fn developer_run_status(status: &developer_run::DeveloperRunStatus) -> serde_json::Value { json!({ "state": status.state, @@ -227,18 +240,21 @@ fn developer_run_snapshot(manager: &developer_run::DeveloperRunManager) -> serde pub(crate) fn handle_native_request( app: &AppHandle, method: &str, - _params: Option, - _deadline: u64, + params: Option, + deadline: u64, ) -> Result { let state = app.state::(); match method { + "browser.probe" | "browser.execute" => state + .browser_controller + .handle_native(app, method, params, deadline), "developer.status" => Ok(developer_run_snapshot(&state.developer_run_manager)), "developer.restart" => state .developer_run_manager .restart() .map(|status| developer_run_status(&status)) .map_err(|error| error.to_string()), - _ => Err(format!("Unsupported native developer request: {method}")), + _ => Err(format!("Unsupported native request: {method}")), } } @@ -372,6 +388,68 @@ async fn developer_run_stop( .map_err(|error| error.to_string()) } +#[tauri::command] +async fn browser_target_register( + webview: tauri::Webview, + app: AppHandle, + state: tauri::State<'_, AppState>, + payload: browser_controller::BrowserTargetRegistration, +) -> Result<(), String> { + require_local_app_webview(&webview, &state)?; + let window = app + .get_webview_window(webview.label()) + .ok_or_else(|| "Browser target requires a local application window".to_string())?; + state.browser_controller.register(&window, payload) +} + +#[tauri::command] +fn browser_target_update( + webview: tauri::Webview, + app: AppHandle, + state: tauri::State<'_, AppState>, + payload: browser_controller::BrowserTargetUpdate, +) -> Result<(), String> { + require_local_app_webview(&webview, &state)?; + state.browser_controller.update(&app, webview.label(), payload) +} + +#[tauri::command] +fn browser_target_action( + webview: tauri::Webview, + app: AppHandle, + state: tauri::State<'_, AppState>, + payload: browser_controller::BrowserTargetAction, +) -> Result<(), String> { + require_local_app_webview(&webview, &state)?; + state.browser_controller.action(&app, webview.label(), payload) +} + +#[tauri::command] +fn browser_target_unregister( + webview: tauri::Webview, + app: AppHandle, + state: tauri::State<'_, AppState>, + registration_id: String, +) -> Result<(), String> { + require_local_app_webview(&webview, &state)?; + state.browser_controller.unregister(&app, webview.label(), ®istration_id) +} + +#[tauri::command] +fn browser_target_claim_open( + webview: tauri::Webview, + app: AppHandle, + state: tauri::State<'_, AppState>, + request_id: String, +) -> Result { + require_local_app_webview(&webview, &state)?; + let claimed = state.browser_controller.claim_open(webview.label(), &request_id); + if claimed { + crate::local_windows::focus(&app, webview.label()); + } + Ok(claimed) +} + fn release_remote_proxy_session_cleanup(app: &AppHandle, session_id: &str) { if let Ok(mut claims) = app.state::().remote_proxy_cleanup_claims.lock() { claims.remove(session_id); @@ -594,6 +672,10 @@ fn should_open_external_url(url: &Url) -> bool { fn intercept_navigation(webview: &Webview, url: &Url) -> bool { let window_label = webview.label().to_string(); + let state = webview.app_handle().state::(); + if let Some(allowed) = state.browser_controller.navigation_allowed(&window_label, url) { + return allowed; + } if should_allow_window_origin(&webview.app_handle(), &window_label, url) { return true; } @@ -1382,6 +1464,9 @@ fn main() { .manage(AppState { manager: CliProcessManager::new(), developer_run_manager: developer_run::DeveloperRunManager::new(), + browser_controller: browser_controller::BrowserController::new( + setup_scope.webview_data_directory.join("browser"), + ), wake_lock: Mutex::new(WakeLockState::default()), remote_navigation: Mutex::new(HashMap::new()), remote_navigation_generation: AtomicU64::new(0), @@ -1477,7 +1562,12 @@ fn main() { set_workspace_menu_enabled, developer_run_get, developer_run_start, - developer_run_stop + developer_run_stop, + browser_target_register, + browser_target_update, + browser_target_action, + browser_target_unregister, + browser_target_claim_open ]) .on_menu_event(|app_handle, event| { match event.id().0.as_str() { @@ -1670,6 +1760,10 @@ fn main() { event: tauri::WindowEvent::Destroyed, .. } => { + app_handle + .state::() + .browser_controller + .remove_window(&app_handle, &label); if let Ok(window_id) = identity::local_window_id(&label) { app_handle .state::() diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index e5706f593..4675cd2a3 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -29,6 +29,7 @@ import { initReleaseNotifications } from "./stores/releases" import { isTauriHost, isWebHost, runtimeEnv } from "./lib/runtime-env" import { useI18n } from "./lib/i18n" import { setWakeLockDesired } from "./lib/native/wake-lock" +import { claimNativeBrowserOpen, onNativeBrowserOpen } from "./lib/native/browser" import { resolveResolvable } from "./lib/commands" import { setWorkspaceMenuEnabled } from "./lib/workspace-open" import { @@ -59,9 +60,11 @@ import { createSession, fetchSessions, loadMessages, + setActiveSessionFromList, updateSessionAgent, updateSessionModel, } from "./stores/sessions" +import { openSessionPreview } from "./stores/session-previews" import { useForegroundRefresh } from "./lib/hooks/use-foreground-refresh" import { messagesLoaded, invalidateSessionMessageLoad } from "./stores/session-state" @@ -261,6 +264,8 @@ const App: Component = () => { }) onMount(() => { + let disposed = false + let browserOpenUnsubscribe = () => {} void initGithubStars() updateInstanceTabBarHeight() const handleResize = () => updateInstanceTabBarHeight() @@ -272,7 +277,33 @@ const App: Component = () => { }) } }, 30_000) + const openRequestedPreview = async (sessionID: string, url: string, requestID: string) => { + const findOwners = () => [...instances().values()].filter((instance) => getSessions(instance.id).some((session) => session.id === sessionID)) + let owners = findOwners() + if (owners.length === 0) { + await Promise.all([...instances().values()].filter((instance) => instance.client).map((instance) => fetchSessions(instance.id, { reset: true }).catch(() => undefined))) + owners = findOwners() + } + if (disposed || owners.length !== 1) { + if (!disposed) log.warn("Failed to route agent-requested web preview", { sessionID, owners: owners.map((instance) => instance.id) }) + return + } + if (!await claimNativeBrowserOpen(requestID)) return + const instance = owners[0] + setShowFolderSelection(false) + selectInstanceTab(instance.id) + setActiveSessionFromList(instance.id, sessionID) + await openSessionPreview(sessionID, url, instance.folder) + } + void onNativeBrowserOpen(({ sessionID, url, requestID }) => { + void openRequestedPreview(sessionID, url, requestID).catch((error) => log.warn("Failed to open agent-requested web preview", { sessionID, error })) + }).then((cleanup) => { + if (disposed) cleanup() + else browserOpenUnsubscribe = cleanup + }).catch((error) => log.warn("Failed to listen for native browser open requests", { error })) onCleanup(() => { + disposed = true + browserOpenUnsubscribe() window.removeEventListener("resize", handleResize) window.clearInterval(livenessTimer) }) diff --git a/packages/ui/src/components/browser-frame-security.test.ts b/packages/ui/src/components/browser-frame-security.test.ts index 28eec2360..5e40d3181 100644 --- a/packages/ui/src/components/browser-frame-security.test.ts +++ b/packages/ui/src/components/browser-frame-security.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict" import { describe, it } from "node:test" -import { getBrowserFramePolicy, getPreviewFrameSource } from "./browser-frame-security.ts" +import { getBrowserFramePolicy, getPreviewFrameSource, isLoopbackPreviewUrl, normalizeBrowserPreviewUrl } from "./browser-frame-security.ts" describe("getBrowserFramePolicy", () => { it("isolates local native frames from the parent bridge", () => { @@ -34,3 +34,23 @@ describe("getBrowserFramePolicy", () => { assert.equal(getPreviewFrameSource({ host: "tauri", windowContext: "local" }, preview, "http://192.168.1.10:9899/"), preview.proxyUrl) }) }) + +describe("isLoopbackPreviewUrl", () => { + it("separates local previews from public browser pages", () => { + assert.equal(isLoopbackPreviewUrl("http://localhost:3000/app"), true) + assert.equal(isLoopbackPreviewUrl("localhost:5173"), true) + assert.equal(isLoopbackPreviewUrl("http://127.0.0.8:5173/"), true) + assert.equal(isLoopbackPreviewUrl("127.0.0.1:5173/app"), true) + assert.equal(isLoopbackPreviewUrl("[::1]:4173"), true) + assert.equal(isLoopbackPreviewUrl("https://github.com/"), false) + }) +}) + +describe("normalizeBrowserPreviewUrl", () => { + it("defaults public hosts to HTTPS and loopback hosts to HTTP", () => { + assert.equal(normalizeBrowserPreviewUrl("github.com"), "https://github.com/") + assert.equal(normalizeBrowserPreviewUrl("localhost:5173/app"), "http://localhost:5173/app") + assert.throws(() => normalizeBrowserPreviewUrl("file:///tmp/index.html")) + assert.throws(() => normalizeBrowserPreviewUrl("https://user:pass@example.com")) + }) +}) diff --git a/packages/ui/src/components/browser-frame-security.ts b/packages/ui/src/components/browser-frame-security.ts index e0c04185f..bab57e94a 100644 --- a/packages/ui/src/components/browser-frame-security.ts +++ b/packages/ui/src/components/browser-frame-security.ts @@ -9,6 +9,29 @@ export function getBrowserFramePolicy(_environment: Pick 2_048) throw new TypeError() + const explicitHttp = /^https?:\/\//i.test(trimmed) + const loopback = isLoopbackPreviewUrl(trimmed) + const target = new URL(explicitHttp ? trimmed : `${loopback ? "http" : "https"}://${trimmed}`) + if (!explicitHttp && /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) { + throw new TypeError() + } + if (!target.hostname || target.username || target.password) throw new TypeError() + return target.href +} + export function getPreviewFrameSource( environment: Pick, preview: { token: string; targetUrl: string; proxyUrl: string }, @@ -18,7 +41,7 @@ export function getPreviewFrameSource( return preview.proxyUrl } const source = new URL(parentUrl) - const loopback = source.hostname === "localhost" || source.hostname === "::1" || /^127(?:\.|$)/.test(source.hostname) + const loopback = source.hostname === "localhost" || source.hostname === "[::1]" || /^127(?:\.|$)/.test(source.hostname) if (source.protocol !== "http:" || !loopback) return preview.proxyUrl const target = new URL(preview.targetUrl) source.hostname = `${preview.token}.preview.localhost` diff --git a/packages/ui/src/components/browser-frame.tsx b/packages/ui/src/components/browser-frame.tsx index 996a1bb52..b9115cb88 100644 --- a/packages/ui/src/components/browser-frame.tsx +++ b/packages/ui/src/components/browser-frame.tsx @@ -1,7 +1,16 @@ import { ArrowLeft, ArrowRight, ChevronDown, Expand, MessageSquarePlus, Monitor, RefreshCw, RotateCw, Smartphone, Tablet } from "lucide-solid" import { Show, createEffect, createMemo, createSignal, onCleanup, untrack, type Component } from "solid-js" import { runtimeEnv } from "../lib/runtime-env" -import { getBrowserFramePolicy } from "./browser-frame-security" +import { + controlTauriBrowserTarget, + nativeBrowserHost, + onTauriBrowserNavigation, + physicalBrowserBounds, + registerTauriBrowserTarget, + unregisterTauriBrowserTarget, + updateTauriBrowserTarget, +} from "../lib/native/browser" +import { getBrowserFramePolicy, normalizeBrowserPreviewUrl } from "./browser-frame-security" export interface BrowserFrameElementTarget { pagePath: string @@ -17,6 +26,7 @@ interface BrowserFrameLabels { back: string refresh: string path: string + invalidUrl?: string go: string commentMode?: string viewport?: string @@ -49,6 +59,7 @@ const VIEWPORT_OPTIONS = [ ] interface BrowserFrameProps { + sessionId?: string title: string initialUrl: string initialAddress?: string @@ -103,12 +114,21 @@ export const BrowserFrame: Component = (props) => { const [viewportMenuOpen, setViewportMenuOpen] = createSignal(false) const [navigating, setNavigating] = createSignal(false) const [highlight, setHighlight] = createSignal<{ x: number; y: number; width: number; height: number } | null>(null) + const browserHost = nativeBrowserHost(runtimeEnv) + const nativeBrowserAvailable = Boolean(browserHost) && props.addressMode === "url" + const [nativeMode, setNativeMode] = createSignal(nativeBrowserAvailable) + const [nativeTarget, setNativeTarget] = createSignal(props.initialAddress ?? "") let iframeRef: HTMLIFrameElement | undefined + let webviewRef: ElectronBrowserWebviewElement | undefined let frameWrapRef: HTMLDivElement | undefined let cleanupFrameListeners: (() => void) | null = null + let cleanupWebviewListeners: (() => void) | null = null + let cleanupTauriTarget: (() => void) | null = null + let tauriRegistered = false + let browserRegistrationId = crypto.randomUUID() const framePolicy = getBrowserFramePolicy(runtimeEnv) - const canComment = createMemo(() => (framePolicy.canInspectDom || props.commentBridge) && Boolean(props.onToggleCommentMode && props.onCommentTarget)) + const canComment = createMemo(() => !nativeMode() && (framePolicy.canInspectDom || props.commentBridge) && Boolean(props.onToggleCommentMode && props.onCommentTarget)) const viewport = createMemo(() => VIEWPORT_PRESETS[viewportPreset()]) const isResponsiveViewport = createMemo(() => viewportPreset() === "responsive") const selectedViewportOption = createMemo(() => VIEWPORT_OPTIONS.find((option) => option.id === viewportPreset()) ?? VIEWPORT_OPTIONS[0]) @@ -267,24 +287,287 @@ export const BrowserFrame: Component = (props) => { attachCommentListeners() } + const reportNativeError = (error: unknown) => props.onNavigationError?.(error) + + const bindWebview = (webview: ElectronBrowserWebviewElement) => { + cleanupWebviewListeners?.() + webviewRef = webview + let active = true + let registered = false + let registeredSessionId = "" + let registering = false + let registrationFailures = 0 + let registrationErrorReported = false + let retryTimer: ReturnType | undefined + const syncLocation = (event: Event) => { + const url = (event as Event & { url?: string }).url ?? webview.getURL() + if (!url) return + setPathInput(url) + props.onFrameLocation?.(url) + } + const reportLoadError = (event: Event) => { + const failure = event as Event & { errorCode?: number; errorDescription?: string } + if (failure.errorCode === -3) return + reportNativeError(new Error(failure.errorDescription || `Browser failed to load (${failure.errorCode ?? "unknown"})`)) + } + const retryRegistration = (error: unknown) => { + registering = false + registrationFailures += 1 + if (registrationFailures >= 5 && !registrationErrorReported) { + registrationErrorReported = true + reportNativeError(error) + } + if (active) retryTimer = setTimeout(syncRegistration, 100) + } + const syncRegistration = () => { + const rect = webview.getBoundingClientRect() + const visible = active && rect.width > 0 && rect.height > 0 + const sessionId = props.sessionId + if (!sessionId || registering) return + if (registered && registeredSessionId !== sessionId) { + const previousId = browserRegistrationId + browserRegistrationId = crypto.randomUUID() + registering = true + registered = false + registeredSessionId = "" + void window.electronAPI?.unregisterBrowserTarget?.(previousId).catch(reportNativeError).finally(() => { + registering = false + if (active) syncRegistration() + }) + return + } + if (visible === registered) return + if (!visible) { + if (retryTimer) clearTimeout(retryTimer) + retryTimer = undefined + if (registered) { + registering = true + void window.electronAPI?.unregisterBrowserTarget?.(browserRegistrationId).catch(reportNativeError).finally(() => { + registering = false + if (active) syncRegistration() + }) + } + registered = false + registeredSessionId = "" + return + } + const register = window.electronAPI?.registerBrowserTarget + if (!register) return + registering = true + try { + void register({ + sessionId, + registrationId: browserRegistrationId, + guestWebContentsId: webview.getWebContentsId(), + }).then(() => { + registering = false + registered = true + registeredSessionId = sessionId + registrationFailures = 0 + registrationErrorReported = false + syncRegistration() + }).catch(retryRegistration) + } catch (error) { + retryRegistration(error) + } + } + const resizeObserver = new ResizeObserver(syncRegistration) + resizeObserver.observe(webview) + createEffect(syncRegistration) + webview.addEventListener("did-navigate", syncLocation) + webview.addEventListener("did-navigate-in-page", syncLocation) + webview.addEventListener("did-fail-load", reportLoadError) + webview.addEventListener("dom-ready", syncRegistration) + syncRegistration() + cleanupWebviewListeners = () => { + active = false + if (retryTimer) clearTimeout(retryTimer) + webview.removeEventListener("did-navigate", syncLocation) + webview.removeEventListener("did-navigate-in-page", syncLocation) + webview.removeEventListener("did-fail-load", reportLoadError) + webview.removeEventListener("dom-ready", syncRegistration) + resizeObserver.disconnect() + if (registered) void window.electronAPI?.unregisterBrowserTarget?.(browserRegistrationId).catch(reportNativeError) + if (webviewRef === webview) webviewRef = undefined + } + } + + const bindTauriPlaceholder = (element: HTMLDivElement) => { + cleanupTauriTarget?.() + let active = true + let registering = false + let registered = false + let nativeVisible = false + let registeredSessionId = "" + let lastBounds = "" + let unlistenNavigation = () => {} + const bounds = () => { + if (viewportMenuOpen() || document.querySelector('[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]')) return null + const rect = element.getBoundingClientRect() + const clip = frameWrapRef?.getBoundingClientRect() ?? rect + const left = Math.max(rect.left, clip.left, 0) + const top = Math.max(rect.top, clip.top, 0) + const right = Math.min(rect.right, clip.right, window.innerWidth) + const bottom = Math.min(rect.bottom, clip.bottom, window.innerHeight) + if (right <= left || bottom <= top) return null + return physicalBrowserBounds({ x: left, y: top, width: right - left, height: bottom - top }, window.devicePixelRatio) + } + const syncBounds = () => { + const sessionId = props.sessionId + if (!active || registering || !sessionId) return + const next = bounds() + if (!next) { + lastBounds = "" + if (registered && nativeVisible) { + registering = true + void updateTauriBrowserTarget(browserRegistrationId, undefined, false).then(() => { + nativeVisible = false + }).catch(reportNativeError).finally(() => { + registering = false + if (active) syncBounds() + }) + } + return + } + if (registered && registeredSessionId !== sessionId) { + const previousId = browserRegistrationId + browserRegistrationId = crypto.randomUUID() + registering = true + unlistenNavigation() + unlistenNavigation = () => {} + registered = false + nativeVisible = false + registeredSessionId = "" + tauriRegistered = false + void unregisterTauriBrowserTarget(previousId).catch(reportNativeError).finally(() => { + registering = false + if (active) syncBounds() + }) + return + } + const serialized = JSON.stringify(next) + if (registered && nativeVisible && serialized === lastBounds) return + if (registered) { + registering = true + void updateTauriBrowserTarget(browserRegistrationId, next, true).then(() => { + lastBounds = serialized + nativeVisible = true + }).catch((error) => { + lastBounds = "" + reportNativeError(error) + }).finally(() => { + registering = false + if (active) syncBounds() + }) + return + } + registering = true + const target = nativeTarget() + const registrationId = browserRegistrationId + void onTauriBrowserNavigation(registrationId, (url) => { + setNativeTarget(url) + setPathInput(url) + props.onFrameLocation?.(url) + }).then(async (unlisten) => { + if (!active) { + unlisten() + return + } + unlistenNavigation() + unlistenNavigation = unlisten + await registerTauriBrowserTarget({ + sessionId, + registrationId, + url: target, + bounds: next, + }) + registeredSessionId = sessionId + lastBounds = serialized + registering = false + if (!active) { + await unregisterTauriBrowserTarget(registrationId) + return + } + registered = true + nativeVisible = true + tauriRegistered = true + if (nativeTarget() !== target) { + await controlTauriBrowserTarget(registrationId, "navigate", nativeTarget()) + } + syncBounds() + }).catch((error) => { + registering = false + lastBounds = "" + reportNativeError(error) + }) + } + const resizeObserver = new ResizeObserver(syncBounds) + const overlayObserver = new MutationObserver(syncBounds) + resizeObserver.observe(element) + overlayObserver.observe(document.body, { attributes: true, childList: true, subtree: true }) + createEffect(syncBounds) + window.addEventListener("resize", syncBounds) + window.addEventListener("scroll", syncBounds, true) + queueMicrotask(syncBounds) + cleanupTauriTarget = () => { + active = false + resizeObserver.disconnect() + overlayObserver.disconnect() + window.removeEventListener("resize", syncBounds) + window.removeEventListener("scroll", syncBounds, true) + unlistenNavigation() + if (registered) void unregisterTauriBrowserTarget(browserRegistrationId).catch(reportNativeError) + registered = false + nativeVisible = false + tauriRegistered = false + } + } + createEffect(() => { const initialUrl = props.initialUrl + const initialAddress = props.initialAddress untrack(() => { setFrameSrc(initialUrl) - setPathInput(props.initialAddress ?? getEditablePathFromUrl(initialUrl)) + const address = initialAddress ?? getEditablePathFromUrl(initialUrl) + setPathInput(address) + if (nativeMode() && webviewRef?.getURL() === address) return + const previousTarget = nativeTarget() + setNativeTarget(address) + const nextNativeMode = nativeBrowserAvailable + if (nextNativeMode && browserHost === "tauri" && tauriRegistered && previousTarget !== address) { + void controlTauriBrowserTarget(browserRegistrationId, "navigate", address).catch(reportNativeError) + } + setNativeMode(nextNativeMode) }) }) + createEffect(() => { + if (nativeMode()) return + cleanupWebviewListeners?.() + cleanupWebviewListeners = null + cleanupTauriTarget?.() + cleanupTauriTarget = null + }) + createEffect(() => { props.commentMode attachCommentListeners() }) - onCleanup(() => cleanupFrameListeners?.()) + onCleanup(() => { + cleanupFrameListeners?.() + cleanupWebviewListeners?.() + cleanupTauriTarget?.() + }) const handleBack = (event: MouseEvent) => { event.preventDefault() event.stopPropagation() + if (nativeMode()) { + if (browserHost === "tauri") void controlTauriBrowserTarget(browserRegistrationId, "back").catch(reportNativeError) + else if (webviewRef?.canGoBack()) webviewRef.goBack() + return + } try { iframeRef?.contentWindow?.history.go(-1) } catch { @@ -293,6 +576,11 @@ export const BrowserFrame: Component = (props) => { } const handleRefresh = () => { + if (nativeMode()) { + if (browserHost === "tauri") void controlTauriBrowserTarget(browserRegistrationId, "reload").catch(reportNativeError) + else webviewRef?.reload() + return + } try { iframeRef?.contentWindow?.location.reload() return @@ -307,10 +595,31 @@ export const BrowserFrame: Component = (props) => { event?.preventDefault() if (props.addressMode === "url" && props.onNavigate) { if (navigating()) return + if (nativeBrowserAvailable) { + try { + const target = normalizeBrowserPreviewUrl(pathInput()) + setPathInput(target) + setNativeTarget(target) + if (nativeMode() && browserHost === "tauri") { + void controlTauriBrowserTarget(browserRegistrationId, "navigate", target).catch(reportNativeError) + } else if (nativeMode() && webviewRef) void webviewRef.loadURL(target).catch(reportNativeError) + else { + setNativeTarget(target) + setNativeMode(true) + } + props.onFrameLocation?.(target) + } catch { + reportNativeError(new Error(props.labels.invalidUrl ?? props.labels.path)) + } + return + } + const restoreNative = nativeMode() + setNativeMode(false) setNavigating(true) try { setFrameSrc(await props.onNavigate(pathInput())) } catch (error) { + setNativeMode(restoreNative) props.onNavigationError?.(error) } finally { setNavigating(false) @@ -420,20 +729,49 @@ export const BrowserFrame: Component = (props) => { ? "absolute inset-0 overflow-hidden bg-surface" : "absolute inset-0 overflow-auto bg-surface-secondary p-4"} > -