- ${this.renderTopbar()} -
- ${this.renderView()} + +
+
+
+
+
+ + +
+
+
GSV
+
+ + ${this.connectionState === "connected" ? "Connected" : + this.connectionState === "connecting" ? "Connecting..." : + "Disconnected"} +
+
+
+
this.disconnect()} title="Disconnect"> +
+
${this.clockText}
+
+
+ + +
+ ${Object.values(this.openWindows).map(ws => this.renderWindow(ws))} +
+ + +
this.closeLauncherOnBackdrop(e)} + > +
+ +
+ ${allTabs + .filter(t => !this.launcherSearch || TAB_LABELS[t].toLowerCase().includes(this.launcherSearch.toLowerCase())) + .map(t => html` + + `)} +
+
+
+ + +
+
+ +
+ ${dockTabs.map(t => { + const isOpen = !!this.openWindows[t]; + const isFocused = isOpen && this.openWindows[t].zIndex === this.topZIndex; + return html` + + `; + })} +
+
+ `; + } + + private renderWindow(ws: WindowState) { + const isChat = ws.tab === "chat"; + const isDragging = this.dragState.active && this.dragState.tab === ws.tab; + + return html` +
this.focusWindow(ws.tab)} + > +
this.startWindowDrag(e, ws.tab)} + @dblclick=${() => this.maximizeWindow(ws.tab)} + > +
+ + + +
+
${TAB_LABELS[ws.tab]}
+
+
+ ${this.renderViewContent(ws.tab)}
`; @@ -1037,10 +1293,21 @@ export class GsvApp extends LitElement { const isConnecting = this.connectionState === "connecting"; return html` + +
+
+
+
+
+
- + + + + +

GSV

Connect to your Gateway

@@ -1097,94 +1364,13 @@ export class GsvApp extends LitElement { ${isConnecting ? html` Connecting...` : "Connect"}
- -
`; } - private renderNav() { - return html` - - `; - } - - private renderTopbar() { - return html` -
-
- -

${TAB_LABELS[this.tab]}

-
-
- -
-
- `; - } - - private renderView() { - switch (this.tab) { + private renderViewContent(tab: Tab) { + switch (tab) { case "chat": return renderChat(this); case "overview": diff --git a/gateway/ui/src/ui/navigation.ts b/gateway/ui/src/ui/navigation.ts index 2466a98..b3d7357 100644 --- a/gateway/ui/src/ui/navigation.ts +++ b/gateway/ui/src/ui/navigation.ts @@ -16,6 +16,7 @@ const TAB_PATHS: Record = { pairing: "/pairing", config: "/config", debug: "/debug", + settings: "/settings", }; const PATH_TABS: Record = Object.fromEntries( diff --git a/gateway/ui/src/ui/types.ts b/gateway/ui/src/ui/types.ts index 4a86de3..5806e9f 100644 --- a/gateway/ui/src/ui/types.ts +++ b/gateway/ui/src/ui/types.ts @@ -197,7 +197,8 @@ export type Tab = | "logs" | "pairing" | "config" - | "debug"; + | "debug" + | "settings"; export const TAB_GROUPS: { label: string; tabs: Tab[] }[] = [ { label: "Chat", tabs: ["chat"] }, @@ -206,18 +207,26 @@ export const TAB_GROUPS: { label: string; tabs: Tab[] }[] = [ { label: "Settings", tabs: ["pairing", "config", "debug"] }, ]; +/** Tabs that appear in the OS dock (settings replaces pairing/config/debug). */ +export const OS_DOCK_TABS: Tab[] = [ + "chat", "overview", "sessions", "channels", "nodes", + "workspace", "cron", "logs", "settings", +]; + +// SVG icon strings (Feather-style, stroke-based) export const TAB_ICONS: Record = { - chat: "\uD83D\uDCAC", - overview: "\uD83D\uDCCA", - sessions: "\uD83D\uDCCB", - channels: "\uD83D\uDCF1", - nodes: "\uD83D\uDDA5\uFE0F", - workspace: "\uD83D\uDCC1", - cron: "\u23F0", - logs: "\uD83D\uDCDC", - pairing: "\uD83E\uDD1D", - config: "\u2699\uFE0F", - debug: "\uD83D\uDD27", + chat: '', + overview: '', + sessions: '', + channels: '', + nodes: '', + workspace: '', + cron: '', + logs: '', + pairing: '', + config: '', + debug: '', + settings: '', }; export const TAB_LABELS: Record = { @@ -232,4 +241,32 @@ export const TAB_LABELS: Record = { pairing: "Pairing", config: "Config", debug: "Debug", + settings: "Settings", +}; + +// Window defaults +export const WINDOW_DEFAULTS: Record = { + chat: { width: 700, height: 550, pinned: true }, + overview: { width: 820, height: 520, pinned: true }, + sessions: { width: 700, height: 480, pinned: true }, + channels: { width: 740, height: 520, pinned: true }, + nodes: { width: 680, height: 460, pinned: false }, + workspace: { width: 920, height: 600, pinned: true }, + cron: { width: 780, height: 520, pinned: false }, + logs: { width: 720, height: 480, pinned: false }, + pairing: { width: 620, height: 420, pinned: false }, + config: { width: 780, height: 560, pinned: false }, + debug: { width: 680, height: 440, pinned: false }, + settings: { width: 800, height: 560, pinned: false }, +}; + +export type WindowState = { + tab: Tab; + x: number; + y: number; + width: number; + height: number; + minimized: boolean; + maximized: boolean; + zIndex: number; }; From 1f13076fbd195e7dee9e478c79792c37371a03dd Mon Sep 17 00:00:00 2001 From: Steve James Date: Wed, 25 Feb 2026 15:03:49 +0100 Subject: [PATCH 4/6] surfaces --- cli/Cargo.lock | 1898 ++++++++++++++++- cli/Cargo.toml | 9 + cli/src/display.rs | 340 +++ cli/src/lib.rs | 2 + cli/src/main.rs | 172 +- cli/src/protocol.rs | 92 + gateway/package.json | 8 +- gateway/src/agents/tools/constants.ts | 3 + gateway/src/agents/tools/index.ts | 7 + gateway/src/agents/tools/surface.ts | 180 ++ gateway/src/gateway/do.ts | 184 +- gateway/src/gateway/rpc-handlers/connect.ts | 8 + gateway/src/gateway/rpc-handlers/index.ts | 12 + gateway/src/gateway/rpc-handlers/surface.ts | 172 ++ gateway/src/protocol/methods.ts | 37 + gateway/src/protocol/surface.ts | 119 ++ gateway/src/protocol/tools.ts | 1 + gateway/src/session/do.ts | 5 +- gateway/ui/src/react/App.tsx | 239 +-- gateway/ui/src/react/components/OsShell.tsx | 567 ++++- gateway/ui/src/react/components/Wallpaper.tsx | 152 ++ gateway/ui/src/react/state/store.ts | 162 +- gateway/ui/src/react/views/ConfigView.tsx | 101 +- gateway/ui/src/react/views/SettingsView.tsx | 53 +- gateway/ui/src/styles/base.css | 38 +- gateway/ui/src/styles/chat.css | 35 + gateway/ui/src/styles/components.css | 56 + gateway/ui/src/styles/layout.css | 14 + gateway/ui/src/styles/os-shell.css | 591 ++++- gateway/ui/src/ui/gateway-client.ts | 37 + gateway/ui/src/ui/storage.ts | 12 + gateway/ui/src/ui/types.ts | 25 + 32 files changed, 4812 insertions(+), 519 deletions(-) create mode 100644 cli/src/display.rs create mode 100644 gateway/src/agents/tools/surface.ts create mode 100644 gateway/src/gateway/rpc-handlers/surface.ts create mode 100644 gateway/src/protocol/surface.ts create mode 100644 gateway/ui/src/react/components/Wallpaper.tsx diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4daf433..184e56a 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -96,7 +96,30 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] @@ -117,6 +140,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -146,6 +175,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -176,6 +214,31 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "cc" version = "1.2.55" @@ -186,6 +249,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfb" version = "0.7.3" @@ -197,6 +266,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -219,7 +298,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -250,10 +329,10 @@ version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -282,6 +361,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.15.11" @@ -314,6 +403,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -324,12 +429,46 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -348,6 +487,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -358,12 +512,61 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.114", +] + [[package]] name = "data-encoding" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + [[package]] name = "digest" version = "0.10.7" @@ -380,7 +583,16 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", ] [[package]] @@ -391,10 +603,38 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -403,9 +643,59 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -434,6 +724,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "filetime" version = "0.2.27" @@ -473,7 +773,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -482,6 +803,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -491,6 +818,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -506,6 +843,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + [[package]] name = "futures-macro" version = "0.3.31" @@ -514,7 +868,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -544,14 +898,119 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "typenum", - "version_check", + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -563,7 +1022,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -581,12 +1040,102 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "gsv" version = "0.0.1" @@ -597,7 +1146,7 @@ dependencies = [ "chrono", "clap", "cliclack", - "dirs", + "dirs 5.0.1", "flate2", "futures-util", "glob", @@ -613,13 +1162,69 @@ dependencies = [ "serde", "serde_json", "sha2", + "tao", "tar", "tokio", "tokio-tungstenite", "toml", + "url", + "urlencoding", "uuid", "walkdir", "whoami", + "wry", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -628,6 +1233,12 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -642,7 +1253,19 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", ] [[package]] @@ -773,7 +1396,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -959,6 +1582,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.85" @@ -980,6 +1625,24 @@ dependencies = [ "serde", ] +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.180" @@ -992,9 +1655,9 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -1009,6 +1672,15 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -1021,12 +1693,58 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1060,7 +1778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -1091,6 +1809,54 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -1100,6 +1866,113 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1118,9 +1991,9 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -1135,7 +2008,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1160,7 +2033,55 @@ dependencies = [ name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] [[package]] name = "percent-encoding" @@ -1198,7 +2119,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1211,6 +2132,126 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1244,6 +2285,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1253,6 +2300,71 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1350,6 +2462,20 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -1371,6 +2497,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -1391,6 +2527,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1409,13 +2554,46 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_syscall" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -1429,6 +2607,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "regex" version = "1.12.2" @@ -1542,13 +2731,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -1620,14 +2818,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", @@ -1643,6 +2847,30 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1670,7 +2898,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1707,6 +2935,16 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1751,6 +2989,18 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.11" @@ -1785,6 +3035,31 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1798,34 +3073,109 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "syn" -version = "2.0.114" +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "system-deps" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", ] [[package]] -name = "sync_wrapper" -version = "1.0.2" +name = "tao" +version = "0.34.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ - "futures-core", + "bitflags 2.10.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "tao-macros" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1839,6 +3189,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.24.0" @@ -1852,6 +3208,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -1889,7 +3256,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1900,7 +3267,38 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", ] [[package]] @@ -1952,7 +3350,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1995,44 +3393,78 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", + "toml_datetime 0.6.3", + "winnow 0.5.40", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] [[package]] name = "tower" @@ -2055,7 +3487,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2155,6 +3587,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.2.2" @@ -2185,6 +3623,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -2220,6 +3664,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -2245,6 +3695,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2312,7 +3768,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -2363,6 +3819,42 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + [[package]] name = "whoami" version = "1.6.1" @@ -2374,6 +3866,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2383,6 +3891,47 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2391,9 +3940,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -2404,7 +3964,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2415,22 +3975,56 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -2439,7 +4033,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -2484,7 +4087,22 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -2524,7 +4142,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -2535,6 +4153,30 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2553,6 +4195,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2571,6 +4219,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2601,6 +4255,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2619,6 +4279,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2637,6 +4303,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2655,6 +4327,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2673,6 +4351,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.14" @@ -2694,6 +4381,65 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wry" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +dependencies = [ + "base64", + "block2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "html5ever", + "http", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "tao-macros", + "thiserror 2.0.18", + "url", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "xattr" version = "1.6.1" @@ -2723,7 +4469,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -2744,7 +4490,7 @@ checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2764,7 +4510,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -2785,7 +4531,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2818,7 +4564,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 87072f5..9fea8e1 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -48,6 +48,12 @@ rpassword = "7" # Only needed when rustls feature is enabled rustls_crate = { package = "rustls", version = "0.23", default-features = false, features = ["ring", "std"], optional = true } +# Display feature: native webview windows for surface rendering +tao = { version = "0.34", optional = true } +wry = { version = "0.54", optional = true, default-features = false } +url = { version = "2", optional = true } +urlencoding = { version = "2", optional = true } + [profile.release] strip = true lto = true @@ -70,3 +76,6 @@ rustls = [ "reqwest/rustls-tls", "rustls_crate", ] + +# Enable native webview display for node surfaces (--display flag) +display = ["dep:tao", "dep:wry", "dep:url", "dep:urlencoding"] diff --git a/cli/src/display.rs b/cli/src/display.rs new file mode 100644 index 0000000..f704438 --- /dev/null +++ b/cli/src/display.rs @@ -0,0 +1,340 @@ +//! Display module — native webview windows for surface rendering. +//! +//! When a node runs with `--display`, this module creates a tao event loop +//! on the main thread and manages wry webview windows for each surface +//! that targets this node. +//! +//! Architecture: +//! Main thread: tao EventLoop (blocks forever, manages windows) +//! Background: tokio runtime (WebSocket connection, tool execution) +//! Communication: EventLoopProxy (Send, thread-safe) + +use std::collections::HashMap; + +use tao::{ + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowBuilder, WindowId}, +}; +use wry::{WebView, WebViewBuilder}; + +// ── Display Events ── + +/// Events sent from the tokio async runtime to the main thread event loop. +#[derive(Debug)] +pub enum DisplayEvent { + /// Open a new surface as a webview window. + OpenSurface { + surface_id: String, + url: String, + label: String, + }, + /// Close an existing surface window. + CloseSurface { surface_id: String }, + /// Update an existing surface (title). + UpdateSurface { + surface_id: String, + label: Option, + }, + /// Shut down the display event loop. + Shutdown, +} + +// ── Display Handle (async-safe sender) ── + +/// Cloneable handle for sending display events from any thread. +/// Wraps tao's EventLoopProxy which is Send + Sync. +#[derive(Clone)] +pub struct DisplayHandle { + proxy: EventLoopProxy, +} + +impl DisplayHandle { + pub fn open_surface(&self, surface_id: String, url: String, label: String) { + let _ = self.proxy.send_event(DisplayEvent::OpenSurface { + surface_id, + url, + label, + }); + } + + pub fn close_surface(&self, surface_id: String) { + let _ = self + .proxy + .send_event(DisplayEvent::CloseSurface { surface_id }); + } + + pub fn update_surface(&self, surface_id: String, label: Option) { + let _ = self + .proxy + .send_event(DisplayEvent::UpdateSurface { surface_id, label }); + } + + pub fn shutdown(&self) { + let _ = self.proxy.send_event(DisplayEvent::Shutdown); + } +} + +// ── Constructors ── + +/// Create the display event loop and return a handle for async communication. +/// Call this on the main thread before spawning the tokio runtime. +pub fn create_display() -> (DisplayHandle, EventLoop) { + let event_loop = EventLoopBuilder::::with_user_event().build(); + let proxy = event_loop.create_proxy(); + (DisplayHandle { proxy }, event_loop) +} + +// ── URL Resolution ── + +/// Convert a WebSocket gateway URL to an HTTP URL for loading the web UI. +pub fn gateway_http_url(ws_url: &str) -> String { + ws_url + .replace("wss://", "https://") + .replace("ws://", "http://") + .trim_end_matches("/ws") + .to_string() +} + +/// Normalize a URL to its embeddable form for known services. +/// Native webviews don't have X-Frame-Options restrictions, but embed URLs +/// give us autoplay and a cleaner player UI. +pub fn to_embed_url(raw: &str) -> String { + // Parse or return as-is + let Ok(url) = url::Url::parse(raw) else { + return raw.to_string(); + }; + let host = url + .host_str() + .unwrap_or("") + .trim_start_matches("www.") + .trim_start_matches("m."); + + // YouTube + if host == "youtube.com" { + // watch?v=ID + if let Some(vid) = url + .query_pairs() + .find(|(k, _)| k == "v") + .map(|(_, v)| v.to_string()) + { + return format!("https://www.youtube.com/embed/{}?autoplay=1", vid); + } + // /shorts/ID + if let Some(rest) = url.path().strip_prefix("/shorts/") { + let id = rest.split('/').next().unwrap_or(rest); + if !id.is_empty() { + return format!("https://www.youtube.com/embed/{}?autoplay=1", id); + } + } + // Already /embed/ — add autoplay if missing + if url.path().starts_with("/embed/") { + if url.query().map_or(true, |q| !q.contains("autoplay")) { + let sep = if url.query().is_some() { "&" } else { "?" }; + return format!("{}{}autoplay=1", raw, sep); + } + return raw.to_string(); + } + } + if host == "youtu.be" { + let id = url + .path() + .trim_start_matches('/') + .split('/') + .next() + .unwrap_or(""); + if !id.is_empty() { + return format!("https://www.youtube.com/embed/{}?autoplay=1", id); + } + } + + // Vimeo + if host == "vimeo.com" { + if let Some(id) = url.path().trim_start_matches('/').split('/').next() { + if id.chars().all(|c| c.is_ascii_digit()) && !id.is_empty() { + return format!("https://player.vimeo.com/video/{}?autoplay=1", id); + } + } + } + if host == "player.vimeo.com" { + if url.query().map_or(true, |q| !q.contains("autoplay")) { + let sep = if url.query().is_some() { "&" } else { "?" }; + return format!("{}{}autoplay=1", raw, sep); + } + return raw.to_string(); + } + + // Spotify + if host == "open.spotify.com" && !url.path().starts_with("/embed/") { + return format!("https://open.spotify.com/embed{}", url.path()); + } + + // Figma + if host == "figma.com" + && (url.path().starts_with("/file/") || url.path().starts_with("/design/")) + { + return format!( + "https://www.figma.com/embed?embed_host=gsv&url={}", + urlencoding::encode(raw) + ); + } + + // Loom + if host == "loom.com" { + if let Some(rest) = url.path().strip_prefix("/share/") { + let id = rest + .split('/') + .next() + .unwrap_or(rest) + .split('?') + .next() + .unwrap_or(rest); + if !id.is_empty() { + return format!("https://www.loom.com/embed/{}?autoplay=1", id); + } + } + } + + raw.to_string() +} + +/// Resolve the URL to load in a webview for a given surface. +/// Unlike the web UI (which needs embed URLs for iframe X-Frame-Options), +/// native wry webviews are full browser contexts that can load any URL directly. +pub fn resolve_surface_url(ws_url: &str, kind: &str, content_ref: &str) -> String { + match kind { + "webview" | "media" => content_ref.to_string(), + "app" => { + let base = gateway_http_url(ws_url); + format!("{}/?shell=os&tab={}", base, content_ref) + } + _ => content_ref.to_string(), + } +} + +// ── Event Loop ── + +struct SurfaceWindow { + window: Window, + _webview: WebView, +} + +/// Run the display event loop. **Blocks the calling thread forever.** +/// Must be called on the main thread (macOS Cocoa requirement). +pub fn run_display_loop(event_loop: EventLoop) -> ! { + let mut surfaces: HashMap = HashMap::new(); + let mut window_to_surface: HashMap = HashMap::new(); + + event_loop.run(move |event, target, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::UserEvent(display_event) => { + handle_display_event( + display_event, + target, + &mut surfaces, + &mut window_to_surface, + control_flow, + ); + } + Event::WindowEvent { + window_id, + event: WindowEvent::CloseRequested, + .. + } => { + if let Some(surface_id) = window_to_surface.remove(&window_id) { + surfaces.remove(&surface_id); + eprintln!("[display] Window closed by user: {}", surface_id); + } + } + _ => {} + } + }) +} + +fn handle_display_event( + event: DisplayEvent, + target: &EventLoopWindowTarget, + surfaces: &mut HashMap, + window_to_surface: &mut HashMap, + control_flow: &mut ControlFlow, +) { + match event { + DisplayEvent::OpenSurface { + surface_id, + url, + label, + } => { + // Close existing surface with the same ID (replace) + if let Some(old) = surfaces.remove(&surface_id) { + window_to_surface.remove(&old.window.id()); + } + + let window = match WindowBuilder::new() + .with_title(&label) + .with_inner_size(LogicalSize::new(1024.0, 768.0)) + .build(target) + { + Ok(w) => w, + Err(e) => { + eprintln!( + "[display] Failed to create window for surface {}: {}", + surface_id, e + ); + return; + } + }; + + // Native webviews are full browser contexts — no iframe restrictions. + // Load the original URL directly (no embed conversion needed). + let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"; + + let webview = match WebViewBuilder::new() + .with_url(&url) + .with_user_agent(ua) + .with_autoplay(true) + .build(&window) + { + Ok(wv) => wv, + Err(e) => { + eprintln!( + "[display] Failed to create webview for surface {}: {}", + surface_id, e + ); + return; + } + }; + + eprintln!("[display] Opened surface {} -> {}", surface_id, url); + let window_id = window.id(); + window_to_surface.insert(window_id, surface_id.clone()); + surfaces.insert( + surface_id, + SurfaceWindow { + window, + _webview: webview, + }, + ); + } + DisplayEvent::CloseSurface { surface_id } => { + if let Some(sw) = surfaces.remove(&surface_id) { + window_to_surface.remove(&sw.window.id()); + eprintln!("[display] Closed surface {}", surface_id); + } + } + DisplayEvent::UpdateSurface { surface_id, label } => { + if let Some(sw) = surfaces.get(&surface_id) { + if let Some(label) = label { + sw.window.set_title(&label); + eprintln!("[display] Updated surface {} title: {}", surface_id, label); + } + } + } + DisplayEvent::Shutdown => { + eprintln!("[display] Shutdown requested"); + *control_flow = ControlFlow::Exit; + } + } +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 5a3ad8c..0e99bf5 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,6 +2,8 @@ pub mod build_info; pub mod config; pub mod connection; pub mod deploy; +#[cfg(feature = "display")] +pub mod display; pub mod gateway_client; pub mod logger; pub mod protocol; diff --git a/cli/src/main.rs b/cli/src/main.rs index 002b92e..171697b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -64,6 +64,11 @@ enum Commands { #[arg(long)] foreground: bool, + /// Enable display server mode: open native webview windows for surfaces. + /// Requires --foreground. Compile with --features display. + #[arg(long)] + display: bool, + /// Node ID (default: hostname) - used as namespace prefix for tools #[arg(long)] id: Option, @@ -799,16 +804,136 @@ fn main() -> Result<(), Box> { .expect("Failed to install rustls crypto provider"); } - // Now start tokio runtime and run async main + // Parse CLI early to detect display mode (needs main thread for tao event loop) + let cli = Cli::parse(); + + // Display mode: tao event loop on main thread, tokio on background thread. + // Must be handled before creating the tokio runtime because macOS Cocoa + // requires the event loop to run on the main thread. + #[cfg(feature = "display")] + if let Commands::Node { + display: true, + id, + workspace, + action: None, + .. + } = &cli.command + { + let cfg = CliConfig::load(); + let url = cli + .url + .clone() + .unwrap_or_else(|| cfg.gateway_url()); + let token = cli.token.clone().or_else(|| cfg.gateway_token()); + let node_id = resolve_node_id(id.clone(), &cfg); + let workspace_path = resolve_node_workspace(workspace.clone(), &cfg); + return run_node_display_main(&url, token, node_id, workspace_path); + } + + // Standard path: tokio runtime on main thread tokio::runtime::Builder::new_multi_thread() .enable_all() .build()? - .block_on(async_main()) + .block_on(async_main_with(cli)) } -async fn async_main() -> Result<(), Box> { - let cli = Cli::parse(); +/// Display mode entry point: tao event loop on main thread, tokio on background thread. +/// This function never returns normally (tao's event loop diverges). +#[cfg(feature = "display")] +fn run_node_display_main( + url: &str, + token: Option, + node_id: String, + workspace: PathBuf, +) -> Result<(), Box> { + use gsv::display::{create_display, run_display_loop}; + + let (display_handle, event_loop) = create_display(); + + eprintln!( + "[display] Starting display node '{}' -> {}", + node_id, url + ); + + // Spawn tokio runtime on a background thread. + // The main thread is reserved for the tao/wry event loop (macOS requirement). + let url_owned = url.to_string(); + let display_for_thread = display_handle.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to build tokio runtime for display node"); + + rt.block_on(async move { + // Channel for surface events from the node event handler + let (surface_tx, mut surface_rx) = + tokio::sync::mpsc::unbounded_channel::<(String, Option)>(); + + // Task that receives surface events and forwards to tao display loop + let display = display_for_thread.clone(); + let gw_url = url_owned.clone(); + tokio::spawn(async move { + while let Some((event_name, payload)) = surface_rx.recv().await { + process_surface_event(&display, &gw_url, &event_name, payload); + } + }); + + if let Err(e) = + run_node(&url_owned, token, node_id, workspace, true, Some(surface_tx)).await + { + eprintln!("[display] Node error: {}", e); + display_for_thread.shutdown(); + } + }); + }); + + // Run tao event loop on main thread (blocks forever) + run_display_loop(event_loop); +} + +/// Process a surface event from the gateway and forward to the display handle. +#[cfg(feature = "display")] +fn process_surface_event( + display: &gsv::display::DisplayHandle, + gateway_url: &str, + event_name: &str, + payload: Option, +) { + use gsv::display::resolve_surface_url; + use gsv::protocol::{SurfaceClosedPayload, SurfaceOpenedPayload, SurfaceUpdatedPayload}; + + let Some(payload) = payload else { + return; + }; + + match event_name { + "surface.opened" => { + if let Ok(data) = serde_json::from_value::(payload) { + let url = resolve_surface_url( + gateway_url, + &data.surface.kind, + &data.surface.content_ref, + ); + display.open_surface(data.surface.surface_id, url, data.surface.label); + } + } + "surface.closed" => { + if let Ok(data) = serde_json::from_value::(payload) { + display.close_surface(data.surface_id); + } + } + "surface.updated" => { + if let Ok(data) = serde_json::from_value::(payload) { + let label = Some(data.surface.label.clone()); + display.update_surface(data.surface.surface_id, label); + } + } + _ => {} + } +} +async fn async_main_with(cli: Cli) -> Result<(), Box> { // Load config from file let cfg = CliConfig::load(); @@ -831,10 +956,20 @@ async fn async_main() -> Result<(), Box> { } Commands::Node { foreground, + display, id, workspace, action, } => { + if display && !foreground { + return Err("--display requires --foreground".into()); + } + #[cfg(not(feature = "display"))] + if display { + return Err( + "--display requires the 'display' feature. Rebuild with: cargo build --features display".into(), + ); + } if let Some(action) = action { if foreground { return Err( @@ -850,7 +985,7 @@ async fn async_main() -> Result<(), Box> { } else if foreground { let node_id = resolve_node_id(id, &cfg); let workspace = resolve_node_workspace(workspace, &cfg); - run_node(&url, token, node_id, workspace).await + run_node(&url, token, node_id, workspace, display, None).await } else { run_node_default_managed( &cfg, @@ -3077,6 +3212,7 @@ fn capabilities_for_tool(tool_name: &str) -> Result, String> { fn build_execution_node_runtime( tool_defs: &[ToolDefinition], + display: bool, ) -> Result> { let mut seen_tool_names = HashSet::new(); let mut host_capabilities = HashSet::new(); @@ -3111,6 +3247,11 @@ fn build_execution_node_runtime( host_capabilities.insert(capability.to_string()); } + // Advertise display capability when running with --display + if display { + host_capabilities.insert("display.surface".to_string()); + } + let mut normalized_host_capabilities: Vec = host_capabilities.into_iter().collect(); normalized_host_capabilities.sort(); @@ -3267,6 +3408,8 @@ async fn run_node( token: Option, node_id: String, workspace: PathBuf, + display_mode: bool, + surface_tx: Option)>>, ) -> Result<(), Box> { let logger = NodeLogger::new(&node_id, &workspace)?; let log_path = logger::node_log_path()?; @@ -3326,13 +3469,14 @@ async fn run_node( let tools = all_tools_with_workspace(workspace.clone()); let tool_defs: Vec<_> = tools.iter().map(|t| t.definition()).collect(); let tool_names: Vec = tool_defs.iter().map(|t| t.name.clone()).collect(); - let node_runtime = build_execution_node_runtime(&tool_defs)?; + let node_runtime = build_execution_node_runtime(&tool_defs, display_mode)?; logger.info( "tools.register", json!({ "toolCount": tool_names.len(), "tools": tool_names, + "displayMode": display_mode, }), ); @@ -3395,6 +3539,7 @@ async fn run_node( let logger_clone = logger.clone(); let coordinator_for_events = transfer_coordinator.clone(); let workspace_for_transfers = workspace.clone(); + let surface_tx_clone = surface_tx.clone(); conn.set_event_handler(move |frame| { let conn = conn_clone.clone(); @@ -3402,6 +3547,7 @@ async fn run_node( let logger = logger_clone.clone(); let coordinator = coordinator_for_events.clone(); let transfer_workspace = workspace_for_transfers.clone(); + let surface_tx = surface_tx_clone.clone(); tokio::spawn(async move { if let Frame::Evt(evt) = frame { @@ -3659,6 +3805,20 @@ async fn run_node( } } } + } else if evt.event.starts_with("surface.") { + // Forward surface events to display module (if active) + if let Some(ref tx) = surface_tx { + let event_name = evt.event.clone(); + if let Err(e) = tx.send((event_name.clone(), evt.payload.clone())) { + logger.warn( + "surface.event.forward_failed", + json!({ + "event": event_name, + "error": e.to_string(), + }), + ); + } + } } } }); diff --git a/cli/src/protocol.rs b/cli/src/protocol.rs index 77884c4..8e7891c 100644 --- a/cli/src/protocol.rs +++ b/cli/src/protocol.rs @@ -230,6 +230,98 @@ pub struct TransferDoneParams { pub error: Option, } +// ── Surface Protocol ── + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceRect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Surface { + pub surface_id: String, + pub kind: String, // "app" | "media" | "component" | "webview" + pub label: String, + pub content_ref: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content_data: Option, + pub target_client_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_session_key: Option, + pub state: String, // "open" | "minimized" | "closed" + #[serde(skip_serializing_if = "Option::is_none")] + pub rect: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub z_index: Option, + pub created_at: f64, + pub updated_at: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceOpenParams { + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + pub content_ref: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rect: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceCloseParams { + pub surface_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceUpdateParams { + pub surface_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rect: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub z_index: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content_data: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceOpenedPayload { + pub surface: Surface, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceClosedPayload { + pub surface_id: String, + pub target_client_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceUpdatedPayload { + pub surface: Surface, +} + impl RequestFrame { pub fn new(method: &str, params: Option) -> Self { Self { diff --git a/gateway/package.json b/gateway/package.json index c71d066..437e7fd 100644 --- a/gateway/package.json +++ b/gateway/package.json @@ -4,14 +4,10 @@ "private": true, "type": "module", "scripts": { - "deploy": "bun alchemy/cli.ts", - "deploy:up": "bun alchemy/cli.ts up", - "deploy:wizard": "bun alchemy/cli.ts wizard", - "deploy:destroy": "bun alchemy/cli.ts destroy", - "deploy:status": "bun alchemy/cli.ts status", - "deploy:wrangler": "wrangler deploy", + "deploy": "wrangler deploy", "dev": "wrangler dev", "start": "wrangler dev", + "build:ui": "cd ui && npm run build", "cf-typegen": "wrangler types", "test": "vitest", "test:run": "vitest run", diff --git a/gateway/src/agents/tools/constants.ts b/gateway/src/agents/tools/constants.ts index 33cf6e1..4f51f42 100644 --- a/gateway/src/agents/tools/constants.ts +++ b/gateway/src/agents/tools/constants.ts @@ -11,4 +11,7 @@ export const NATIVE_TOOLS = { MESSAGE: `${NATIVE_TOOL_PREFIX}Message`, SESSIONS_LIST: `${NATIVE_TOOL_PREFIX}SessionsList`, SESSION_SEND: `${NATIVE_TOOL_PREFIX}SessionSend`, + OPEN_VIEW: `${NATIVE_TOOL_PREFIX}OpenView`, + LIST_VIEWS: `${NATIVE_TOOL_PREFIX}ListViews`, + CLOSE_VIEW: `${NATIVE_TOOL_PREFIX}CloseView`, } as const; diff --git a/gateway/src/agents/tools/index.ts b/gateway/src/agents/tools/index.ts index 777b8ed..c7c2ba5 100644 --- a/gateway/src/agents/tools/index.ts +++ b/gateway/src/agents/tools/index.ts @@ -18,6 +18,10 @@ import { workspaceNativeToolHandlers, } from "./workspace"; import { getTransferToolDefinitions } from "./transfer"; +import { + getSurfaceToolDefinitions, + surfaceNativeToolHandlers, +} from "./surface"; import type { NativeToolExecutionContext, NativeToolHandlerMap, @@ -32,6 +36,7 @@ export * from "./gateway"; export * from "./message"; export * from "./sessions"; export * from "./transfer"; +export * from "./surface"; const nativeToolHandlers: NativeToolHandlerMap = { ...workspaceNativeToolHandlers, @@ -39,6 +44,7 @@ const nativeToolHandlers: NativeToolHandlerMap = { ...cronNativeToolHandlers, ...messageNativeToolHandlers, ...sessionsNativeToolHandlers, + ...surfaceNativeToolHandlers, }; export function isNativeTool(toolName: string): boolean { @@ -53,6 +59,7 @@ export function getNativeToolDefinitions(): ToolDefinition[] { ...getMessageToolDefinitions(), ...getSessionsToolDefinitions(), ...getTransferToolDefinitions(), + ...getSurfaceToolDefinitions(), ]; } diff --git a/gateway/src/agents/tools/surface.ts b/gateway/src/agents/tools/surface.ts new file mode 100644 index 0000000..0c57bae --- /dev/null +++ b/gateway/src/agents/tools/surface.ts @@ -0,0 +1,180 @@ +import { NATIVE_TOOLS } from "./constants"; +import type { ToolDefinition } from "../../protocol/tools"; +import type { NativeToolHandlerMap } from "./types"; +import type { Surface } from "../../protocol/surface"; + +export const getSurfaceToolDefinitions = (): ToolDefinition[] => [ + { + name: NATIVE_TOOLS.OPEN_VIEW, + description: + "Open a view on a connected client or node. Use this to display apps, media, or web content on the user's screen. " + + "If targetClientId is omitted: webview/media surfaces prefer display-capable nodes (native windows); " + + "app surfaces prefer web clients. " + + "kind=app opens a built-in app tab (chat, sessions, channels, nodes, workspace, cron, logs, settings, overview). " + + "kind=media opens a media player (contentRef should be a URL). " + + "kind=webview opens an arbitrary URL.", + inputSchema: { + type: "object", + properties: { + kind: { + type: "string", + enum: ["app", "media", "webview"], + description: "Type of view to open.", + }, + contentRef: { + type: "string", + description: + "What to display. For kind=app, this is the tab name (e.g., 'chat', 'sessions'). " + + "For kind=media or kind=webview, this is a URL.", + }, + label: { + type: "string", + description: "Optional window title. Defaults to contentRef.", + }, + targetClientId: { + type: "string", + description: + "Optional ID of the client or node to open the view on. " + + "If omitted, webview/media views auto-target display nodes; app views auto-target web clients.", + }, + }, + required: ["kind", "contentRef"], + }, + }, + { + name: NATIVE_TOOLS.LIST_VIEWS, + description: + "List all currently open views (surfaces) across all connected clients. " + + "Use this to see what the user is looking at, or to find a surfaceId to close.", + inputSchema: { + type: "object", + properties: { + targetClientId: { + type: "string", + description: + "Optional. Filter views to a specific client/node.", + }, + }, + required: [], + }, + }, + { + name: NATIVE_TOOLS.CLOSE_VIEW, + description: + "Close a view (surface) by its surfaceId. Use gsv__ListViews first to find the surfaceId.", + inputSchema: { + type: "object", + properties: { + surfaceId: { + type: "string", + description: "The surfaceId of the view to close.", + }, + }, + required: ["surfaceId"], + }, + }, +]; + +export const surfaceNativeToolHandlers: NativeToolHandlerMap = { + [NATIVE_TOOLS.OPEN_VIEW]: async (context, args) => { + if (!context.gateway) { + return { + ok: false, + error: "OpenView tool unavailable: gateway context missing", + }; + } + + const kind = typeof args.kind === "string" ? args.kind : "app"; + const contentRef = + typeof args.contentRef === "string" ? args.contentRef : ""; + const label = + typeof args.label === "string" ? args.label : undefined; + const targetClientId = + typeof args.targetClientId === "string" + ? args.targetClientId + : undefined; + + if (!contentRef) { + return { ok: false, error: "contentRef is required" }; + } + + const result = (await context.gateway.openSurface({ + kind, + contentRef, + label, + targetClientId, + })) as unknown as { ok: boolean; surface?: Surface; error?: string }; + + if (!result.ok) { + return { ok: false, error: result.error }; + } + + return { + ok: true, + result: { + surfaceId: result.surface?.surfaceId, + kind: result.surface?.kind, + contentRef: result.surface?.contentRef, + targetClientId: result.surface?.targetClientId, + label: result.surface?.label, + }, + }; + }, + + [NATIVE_TOOLS.LIST_VIEWS]: async (context, args) => { + if (!context.gateway) { + return { + ok: false, + error: "ListViews tool unavailable: gateway context missing", + }; + } + + const targetClientId = + typeof args.targetClientId === "string" + ? args.targetClientId + : undefined; + + const result = (await context.gateway.listSurfaces( + targetClientId, + )) as unknown as { surfaces: Surface[]; count: number }; + + return { + ok: true, + result: { + count: result.count, + surfaces: result.surfaces.map((s: Surface) => ({ + surfaceId: s.surfaceId, + kind: s.kind, + label: s.label, + contentRef: s.contentRef, + targetClientId: s.targetClientId, + state: s.state, + })), + }, + }; + }, + + [NATIVE_TOOLS.CLOSE_VIEW]: async (context, args) => { + if (!context.gateway) { + return { + ok: false, + error: "CloseView tool unavailable: gateway context missing", + }; + } + + const surfaceId = + typeof args.surfaceId === "string" ? args.surfaceId : ""; + if (!surfaceId) { + return { ok: false, error: "surfaceId is required" }; + } + + const result = (await context.gateway.closeSurface( + surfaceId, + )) as unknown as { ok: boolean; error?: string }; + if (!result.ok) { + return { ok: false, error: result.error }; + } + + return { ok: true, result: { closed: true, surfaceId } }; + }, +}; diff --git a/gateway/src/gateway/do.ts b/gateway/src/gateway/do.ts index 3abc018..55e6493 100644 --- a/gateway/src/gateway/do.ts +++ b/gateway/src/gateway/do.ts @@ -1,6 +1,6 @@ import { DurableObject } from "cloudflare:workers"; import type { ChannelWorkerInterface } from "../channel-interface"; -import { PersistedObject, snapshot } from "../shared/persisted-object"; +import { PersistedObject, snapshot, type Proxied } from "../shared/persisted-object"; import type { Frame, EventFrame, @@ -61,6 +61,7 @@ import { type CronRunResult, } from "../cron"; import type { ChatEventPayload } from "../protocol/chat"; +import type { Surface } from "../protocol/surface"; import type { ChannelRegistryEntry, ChannelId, @@ -149,6 +150,12 @@ export class Gateway extends DurableObject { }, ); + // Surface registry — renderable views across all clients + readonly surfaces = PersistedObject>( + this.ctx.storage.kv, + { prefix: "surfaces:" }, + ); + // Heartbeat scheduler state (persisted to survive DO eviction) readonly heartbeatScheduler: { initialized: boolean } = PersistedObject<{ initialized: boolean; @@ -345,6 +352,8 @@ export class Gateway extends DurableObject { } this.clients.delete(clientId); this.nodeService.cleanupClientPendingOperations(clientId); + // Cleanup surfaces targeting this disconnected client. + this.cleanupSurfacesForClient(clientId); } else if (mode === "node" && nodeId) { // Ignore close events from stale sockets that were replaced by reconnect. if (this.nodes.get(nodeId) !== ws) { @@ -360,6 +369,7 @@ export class Gateway extends DurableObject { `Node disconnected during log request: ${nodeId}`, ); failTransfersForNode(this, nodeId); + this.cleanupSurfacesForClient(nodeId); console.log(`[Gateway] Node ${nodeId} marked offline`); } else if (mode === "channel" && channelKey) { // Ignore close events from stale sockets that were replaced by reconnect. @@ -828,6 +838,71 @@ export class Gateway extends DurableObject { return this.getFullConfig(); } + /** Broadcast an event frame to all connected clients. */ + broadcastToClients(event: string, payload: T): void { + const evt: EventFrame = { type: "evt", event, payload }; + const message = JSON.stringify(evt); + for (const ws of this.clients.values()) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(message); + } + } + } + + /** + * Broadcast a surface event to all web clients AND the targeted node + * (if the surface targets a display-capable node). + * + * @param excludeWs - Skip this WebSocket (the requesting client already + * gets the data from the RPC response, so echoing the event back causes + * duplicate windows in the OS shell). + */ + broadcastSurfaceEvent(event: string, payload: T, targetClientId?: string, excludeWs?: WebSocket): void { + const evt: EventFrame = { type: "evt", event, payload }; + const message = JSON.stringify(evt); + + // Broadcast to all web clients except the sender + for (const ws of this.clients.values()) { + if (ws === excludeWs) continue; + if (ws.readyState === WebSocket.OPEN) { + ws.send(message); + } + } + + // Also send to the targeted node if it's a node (not a web client) + if (targetClientId && this.nodes.has(targetClientId)) { + const nodeWs = this.nodes.get(targetClientId); + if (nodeWs && nodeWs !== excludeWs && nodeWs.readyState === WebSocket.OPEN) { + nodeWs.send(message); + } + } + } + + /** Check if a node has the display.surface capability. */ + nodeHasDisplayCapability(nodeId: string): boolean { + const runtime = this.nodeService.getNodeRuntime(nodeId); + return runtime?.hostCapabilities?.includes("display.surface") ?? false; + } + + /** Find the first display-capable node. */ + findDisplayNode(): string | null { + for (const nodeId of this.nodes.keys()) { + if (this.nodeHasDisplayCapability(nodeId)) { + return nodeId; + } + } + return null; + } + + /** Clean up surfaces targeting a disconnected client or node. */ + cleanupSurfacesForClient(clientId: string): void { + for (const [surfaceId, surface] of Object.entries(this.surfaces)) { + if (surface.targetClientId === clientId) { + delete this.surfaces[surfaceId]; + } + } + } + broadcastToSession(sessionKey: string, payload: ChatEventPayload): void { const evt: EventFrame = { type: "evt", @@ -894,6 +969,113 @@ export class Gateway extends DurableObject { } } + // ---- Surface System (agent tool entry points) ---- + + /** + * Open a surface on a specific client or the first available client. + * Called by Session DO via gateway stub (for agent tools like gsv__OpenView). + */ + async openSurface(params: { + kind: string; + contentRef: string; + label?: string; + contentData?: unknown; + targetClientId?: string; + sourceSessionKey?: string; + }): Promise<{ ok: boolean; surface?: Surface; error?: string }> { + let targetClientId = params.targetClientId; + + // Auto-target resolution: + // 1. For webview/media surfaces, prefer display-capable nodes (native windows, + // no X-Frame-Options issues, autoplay works). Fall back to web clients. + // 2. For app surfaces, prefer web clients (built-in tab views). + // 3. If nothing is connected, error out. + if (!targetClientId) { + const isNativeKind = params.kind === "webview" || params.kind === "media"; + const displayNode = this.findDisplayNode(); + const firstClient = this.clients.keys().next(); + + if (isNativeKind && displayNode) { + targetClientId = displayNode; + } else if (!firstClient.done) { + targetClientId = firstClient.value; + } else if (displayNode) { + targetClientId = displayNode; + } else { + return { ok: false, error: "No clients or display-capable nodes connected" }; + } + } + + // Verify target is connected + if (!this.clients.has(targetClientId) && !this.nodes.has(targetClientId)) { + return { ok: false, error: `Target not connected: ${targetClientId}` }; + } + + const now = Date.now(); + const surfaceId = crypto.randomUUID(); + const surface: Surface = { + surfaceId, + kind: params.kind as Surface["kind"], + label: params.label ?? params.contentRef, + contentRef: params.contentRef, + contentData: params.contentData, + targetClientId, + sourceSessionKey: params.sourceSessionKey, + state: "open", + createdAt: now, + updatedAt: now, + }; + + this.surfaces[surfaceId] = surface; + + // Broadcast to all clients + targeted node + this.broadcastSurfaceEvent("surface.opened", { surface }, targetClientId); + + console.log( + `[Gateway] Surface opened via tool: ${surfaceId} kind=${surface.kind} ref=${surface.contentRef} target=${targetClientId}`, + ); + + return { ok: true, surface }; + } + + /** + * List all surfaces, optionally filtered by target client. + * Called by Session DO via gateway stub (for agent tools like gsv__ListViews). + */ + async listSurfaces(targetClientId?: string): Promise<{ + surfaces: Surface[]; + count: number; + }> { + const all = Object.values(this.surfaces); + const filtered = targetClientId + ? all.filter((s) => s.targetClientId === targetClientId) + : all; + // Snapshot proxied values so they serialize correctly across the RPC boundary. + const snapped = filtered.map((s) => + snapshot(s as unknown as Proxied), + ); + return { surfaces: snapped, count: snapped.length }; + } + + /** + * Close a surface by ID. + * Called by Session DO via gateway stub (for agent tools like gsv__CloseView). + */ + async closeSurface(surfaceId: string): Promise<{ ok: boolean; error?: string }> { + const surface = this.surfaces[surfaceId]; + if (!surface) { + return { ok: false, error: `Surface not found: ${surfaceId}` }; + } + + const targetClientId = surface.targetClientId; + delete this.surfaces[surfaceId]; + + this.broadcastSurfaceEvent("surface.closed", { surfaceId, targetClientId }, targetClientId); + + console.log(`[Gateway] Surface closed via tool: ${surfaceId}`); + return { ok: true }; + } + // ---- Heartbeat System ---- private getGatewayAlarmParticipants( diff --git a/gateway/src/gateway/rpc-handlers/connect.ts b/gateway/src/gateway/rpc-handlers/connect.ts index 124500b..b0a3261 100644 --- a/gateway/src/gateway/rpc-handlers/connect.ts +++ b/gateway/src/gateway/rpc-handlers/connect.ts @@ -156,6 +156,11 @@ export const handleConnect: Handler<"connect"> = async (ctx) => { "channel.login", "channel.logout", "channels.list", + "surface.open", + "surface.close", + "surface.update", + "surface.focus", + "surface.list", ], events: [ "chat", @@ -163,6 +168,9 @@ export const handleConnect: Handler<"connect"> = async (ctx) => { "tool.result", "logs.get", "channel.outbound", + "surface.opened", + "surface.closed", + "surface.updated", ], }, }; diff --git a/gateway/src/gateway/rpc-handlers/index.ts b/gateway/src/gateway/rpc-handlers/index.ts index 9f18e10..607537f 100644 --- a/gateway/src/gateway/rpc-handlers/index.ts +++ b/gateway/src/gateway/rpc-handlers/index.ts @@ -58,6 +58,13 @@ import { handleTransferComplete, handleTransferDone, } from "./transfer"; +import { + handleSurfaceOpen, + handleSurfaceClose, + handleSurfaceUpdate, + handleSurfaceFocus, + handleSurfaceList, +} from "./surface"; export function buildRpcHandlers(): Partial<{ [M in RpcMethod]: Handler }> { return { @@ -111,5 +118,10 @@ export function buildRpcHandlers(): Partial<{ [M in RpcMethod]: Handler }> { "transfer.accept": handleTransferAccept, "transfer.complete": handleTransferComplete, "transfer.done": handleTransferDone, + "surface.open": handleSurfaceOpen, + "surface.close": handleSurfaceClose, + "surface.update": handleSurfaceUpdate, + "surface.focus": handleSurfaceFocus, + "surface.list": handleSurfaceList, }; } diff --git a/gateway/src/gateway/rpc-handlers/surface.ts b/gateway/src/gateway/rpc-handlers/surface.ts new file mode 100644 index 0000000..3c7d823 --- /dev/null +++ b/gateway/src/gateway/rpc-handlers/surface.ts @@ -0,0 +1,172 @@ +import { RpcError } from "../../shared/utils"; +import { snapshot, type Proxied } from "../../shared/persisted-object"; +import type { Handler } from "../../protocol/methods"; +import type { Surface } from "../../protocol/surface"; +import type { + SurfaceOpenedPayload, + SurfaceClosedPayload, + SurfaceUpdatedPayload, +} from "../../protocol/surface"; + +/** Cast a PersistedObject value to Proxied for snapshot(). */ +function snap(s: Surface): Surface { + return snapshot(s as unknown as Proxied); +} + +function generateSurfaceId(): string { + return crypto.randomUUID(); +} + +function getCallerClientId(ws: WebSocket): string { + const attachment = ws.deserializeAttachment(); + if (attachment.mode === "client" && attachment.clientId) { + return attachment.clientId; + } + if (attachment.mode === "node" && attachment.nodeId) { + return attachment.nodeId; + } + throw new RpcError(403, "Only clients and nodes can manage surfaces"); +} + +export const handleSurfaceOpen: Handler<"surface.open"> = ({ gw, ws, params }) => { + if (!params?.contentRef) { + throw new RpcError(400, "contentRef is required"); + } + + const callerId = getCallerClientId(ws); + const targetClientId = params.targetClientId ?? callerId; + + // Verify target exists (client or node) + if (!gw.clients.has(targetClientId) && !gw.nodes.has(targetClientId)) { + throw new RpcError( + 404, + `Target client not connected: ${targetClientId}`, + ); + } + + const now = Date.now(); + const surfaceId = generateSurfaceId(); + const surface: Surface = { + surfaceId, + kind: params.kind, + label: params.label ?? params.contentRef, + contentRef: params.contentRef, + contentData: params.contentData, + targetClientId, + sourceClientId: callerId, + state: params.state ?? "open", + rect: params.rect, + createdAt: now, + updatedAt: now, + }; + + gw.surfaces[surfaceId] = surface; + + // Broadcast to other clients + targeted node (exclude sender — they get the RPC response) + gw.broadcastSurfaceEvent("surface.opened", { + surface: snap(surface), + }, targetClientId, ws); + + console.log( + `[Gateway] Surface opened: ${surfaceId} kind=${surface.kind} ref=${surface.contentRef} target=${targetClientId} source=${callerId}`, + ); + + return { surface: snap(surface) }; +}; + +export const handleSurfaceClose: Handler<"surface.close"> = ({ gw, ws, params }) => { + if (!params?.surfaceId) { + throw new RpcError(400, "surfaceId is required"); + } + + const surface = gw.surfaces[params.surfaceId]; + if (!surface) { + throw new RpcError(404, `Surface not found: ${params.surfaceId}`); + } + + const targetClientId = surface.targetClientId; + delete gw.surfaces[params.surfaceId]; + + // Broadcast to other clients + targeted node (exclude sender) + gw.broadcastSurfaceEvent("surface.closed", { + surfaceId: params.surfaceId, + targetClientId, + }, targetClientId, ws); + + console.log(`[Gateway] Surface closed: ${params.surfaceId}`); + + return { ok: true as const, surfaceId: params.surfaceId }; +}; + +export const handleSurfaceUpdate: Handler<"surface.update"> = ({ gw, ws, params }) => { + if (!params?.surfaceId) { + throw new RpcError(400, "surfaceId is required"); + } + + const surface = gw.surfaces[params.surfaceId]; + if (!surface) { + throw new RpcError(404, `Surface not found: ${params.surfaceId}`); + } + + // Apply updates + if (params.state !== undefined) surface.state = params.state; + if (params.rect !== undefined) surface.rect = params.rect; + if (params.label !== undefined) surface.label = params.label; + if (params.zIndex !== undefined) surface.zIndex = params.zIndex; + if (params.contentData !== undefined) surface.contentData = params.contentData; + surface.updatedAt = Date.now(); + + // The PersistedObject proxy auto-persists mutations, + // but we explicitly re-assign to ensure the top-level key triggers a put. + gw.surfaces[params.surfaceId] = surface; + + // Broadcast to other clients + targeted node (exclude sender) + gw.broadcastSurfaceEvent("surface.updated", { + surface: snap(surface), + }, surface.targetClientId, ws); + + return { surface: snap(surface) }; +}; + +export const handleSurfaceFocus: Handler<"surface.focus"> = ({ gw, ws, params }) => { + if (!params?.surfaceId) { + throw new RpcError(400, "surfaceId is required"); + } + + const surface = gw.surfaces[params.surfaceId]; + if (!surface) { + throw new RpcError(404, `Surface not found: ${params.surfaceId}`); + } + + // Compute a zIndex higher than all current surfaces for the same target + let maxZ = 0; + for (const s of Object.values(gw.surfaces)) { + if (s.targetClientId === surface.targetClientId && s.zIndex !== undefined) { + maxZ = Math.max(maxZ, s.zIndex); + } + } + surface.zIndex = maxZ + 1; + surface.state = "open"; // un-minimize on focus + surface.updatedAt = Date.now(); + gw.surfaces[params.surfaceId] = surface; + + gw.broadcastSurfaceEvent("surface.updated", { + surface: snap(surface), + }, surface.targetClientId, ws); + + return { surface: snap(surface) }; +}; + +export const handleSurfaceList: Handler<"surface.list"> = ({ gw, params }) => { + const allSurfaces = Object.values(gw.surfaces); + const targetFilter = params?.targetClientId; + + const filtered = targetFilter + ? allSurfaces.filter((s) => s.targetClientId === targetFilter) + : allSurfaces; + + return { + surfaces: filtered.map((s) => snap(s)), + count: filtered.length, + }; +}; diff --git a/gateway/src/protocol/methods.ts b/gateway/src/protocol/methods.ts index 024fe5c..3114da9 100644 --- a/gateway/src/protocol/methods.ts +++ b/gateway/src/protocol/methods.ts @@ -35,6 +35,18 @@ import type { TransferCompleteParams, TransferDoneParams, } from "./transfer"; +import type { + SurfaceOpenParams, + SurfaceOpenResult, + SurfaceCloseParams, + SurfaceCloseResult, + SurfaceUpdateParams, + SurfaceUpdateResult, + SurfaceFocusParams, + SurfaceFocusResult, + SurfaceListParams, + SurfaceListResult, +} from "./surface"; import type { CronJob, CronJobCreate, @@ -425,6 +437,31 @@ export type RpcMethods = { }; }; + "surface.open": { + params: SurfaceOpenParams; + result: SurfaceOpenResult; + }; + + "surface.close": { + params: SurfaceCloseParams; + result: SurfaceCloseResult; + }; + + "surface.update": { + params: SurfaceUpdateParams; + result: SurfaceUpdateResult; + }; + + "surface.focus": { + params: SurfaceFocusParams; + result: SurfaceFocusResult; + }; + + "surface.list": { + params: SurfaceListParams; + result: SurfaceListResult; + }; + "tool.request": { params: ToolRequestParams; result: { diff --git a/gateway/src/protocol/surface.ts b/gateway/src/protocol/surface.ts new file mode 100644 index 0000000..599eae6 --- /dev/null +++ b/gateway/src/protocol/surface.ts @@ -0,0 +1,119 @@ +/** + * Surface Protocol — renderable views managed by the Gateway display server. + * + * A Surface is the protocol-level abstraction for a renderable view. + * It maps to: + * - A window in the OS-shell web UI + * - A WebView on native node clients (future) + * - A panel in terminal-mode clients (future) + * + * The Gateway maintains the authoritative surface registry; clients + * send requests to open/close/update, and the gateway broadcasts + * state changes to all connected clients via events. + */ + +// ── Surface Kind ── +// What kind of content this surface renders. +export type SurfaceKind = + | "app" // built-in app tab (chat, settings, overview, etc.) + | "media" // media player (video, audio, image) + | "component" // custom agent-rendered component (future) + | "webview"; // arbitrary URL (future) + +// ── Surface State ── +export type SurfaceState = "open" | "minimized" | "closed"; + +// ── Position / size hint ── +export type SurfaceRect = { + x: number; + y: number; + width: number; + height: number; +}; + +// ── Core Surface record ── +export type Surface = { + surfaceId: string; + kind: SurfaceKind; + label: string; + contentRef: string; // tab name, media URL, component ID, or URL + contentData?: unknown; // extra data (component props, media metadata) + targetClientId: string; // which client/node should render this + sourceClientId?: string; // who requested opening it + sourceSessionKey?: string; // if opened by an agent tool + state: SurfaceState; + rect?: SurfaceRect; + zIndex?: number; + createdAt: number; + updatedAt: number; +}; + +// ── RPC params / results ── + +export type SurfaceOpenParams = { + kind: SurfaceKind; + label?: string; + contentRef: string; + contentData?: unknown; + targetClientId?: string; // omit = self + state?: SurfaceState; // default "open" + rect?: SurfaceRect; +}; + +export type SurfaceOpenResult = { + surface: Surface; +}; + +export type SurfaceCloseParams = { + surfaceId: string; +}; + +export type SurfaceCloseResult = { + ok: true; + surfaceId: string; +}; + +export type SurfaceUpdateParams = { + surfaceId: string; + state?: SurfaceState; + rect?: SurfaceRect; + label?: string; + zIndex?: number; + contentData?: unknown; +}; + +export type SurfaceUpdateResult = { + surface: Surface; +}; + +export type SurfaceFocusParams = { + surfaceId: string; +}; + +export type SurfaceFocusResult = { + surface: Surface; +}; + +export type SurfaceListParams = { + targetClientId?: string; // filter by target client +} | undefined; + +export type SurfaceListResult = { + surfaces: Surface[]; + count: number; +}; + +// ── Event payloads ── + +export type SurfaceOpenedPayload = { + surface: Surface; +}; + +export type SurfaceClosedPayload = { + surfaceId: string; + targetClientId: string; +}; + +export type SurfaceUpdatedPayload = { + surface: Surface; +}; diff --git a/gateway/src/protocol/tools.ts b/gateway/src/protocol/tools.ts index 30433d7..c456449 100644 --- a/gateway/src/protocol/tools.ts +++ b/gateway/src/protocol/tools.ts @@ -11,6 +11,7 @@ export const CAPABILITY_IDS = [ "filesystem.edit", "text.search", "shell.exec", + "display.surface", ] as const; export type CapabilityId = (typeof CAPABILITY_IDS)[number]; diff --git a/gateway/src/session/do.ts b/gateway/src/session/do.ts index 5819296..9b642c6 100644 --- a/gateway/src/session/do.ts +++ b/gateway/src/session/do.ts @@ -291,14 +291,11 @@ class MediaCache { } } -export class Session extends Agent { +export class Session extends DurableObject { private static generateSessionId(): string { return crypto.randomUUID(); } - shouldSendProtocolMessages(_: Connection, __: ConnectionContext): boolean { - return false; - } /** * Extract agentId from session key. diff --git a/gateway/ui/src/react/App.tsx b/gateway/ui/src/react/App.tsx index 169dcb2..c7accc8 100644 --- a/gateway/ui/src/react/App.tsx +++ b/gateway/ui/src/react/App.tsx @@ -1,84 +1,32 @@ -import { useEffect, useMemo, useState } from "react"; -import { Badge } from "@cloudflare/kumo/components/badge"; +import { useEffect, useState } from "react"; import { Button } from "@cloudflare/kumo/components/button"; import { Input } from "@cloudflare/kumo/components/input"; import { Select } from "@cloudflare/kumo/components/select"; import { SensitiveInput } from "@cloudflare/kumo/components/sensitive-input"; import { Surface } from "@cloudflare/kumo/components/surface"; import { getGatewayUrl, type UiSettings } from "../ui/storage"; -import { TAB_GROUPS, TAB_ICONS, TAB_LABELS } from "../ui/types"; import { OsShell } from "./components/OsShell"; import { useReactUiStore } from "./state/store"; -import { preloadTabView, TabView } from "./tabViews"; - -type LayoutMode = "classic" | "os"; - -const LAYOUT_MODE_STORAGE_KEY = "gsv-layout-mode"; -const LAYOUT_MODE_QUERY_PARAM = "shell"; - -function getInitialLayoutMode(): LayoutMode { - if (typeof window === "undefined") { - return "classic"; - } - - const searchParams = new URLSearchParams(window.location.search); - if (searchParams.get(LAYOUT_MODE_QUERY_PARAM) === "os") { - return "os"; - } - - const stored = window.localStorage.getItem(LAYOUT_MODE_STORAGE_KEY); - return stored === "os" ? "os" : "classic"; -} - -function persistLayoutMode(mode: LayoutMode): void { - if (typeof window === "undefined") { - return; - } - - window.localStorage.setItem(LAYOUT_MODE_STORAGE_KEY, mode); - const url = new URL(window.location.href); - if (mode === "os") { - url.searchParams.set(LAYOUT_MODE_QUERY_PARAM, "os"); - } else { - url.searchParams.delete(LAYOUT_MODE_QUERY_PARAM); - } - window.history.replaceState( - window.history.state, - "", - `${url.pathname}${url.search}${url.hash}`, - ); -} +import { preloadTabView } from "./tabViews"; export function App() { const initialize = useReactUiStore((s) => s.initialize); const cleanup = useReactUiStore((s) => s.cleanup); const syncTabFromLocation = useReactUiStore((s) => s.syncTabFromLocation); - const setMobileLayout = useReactUiStore((s) => s.setMobileLayout); const showConnectScreen = useReactUiStore((s) => s.showConnectScreen); const tab = useReactUiStore((s) => s.tab); const switchTab = useReactUiStore((s) => s.switchTab); - const isMobileLayout = useReactUiStore((s) => s.isMobileLayout); const connectionState = useReactUiStore((s) => s.connectionState); const updateSettings = useReactUiStore((s) => s.updateSettings); const settings = useReactUiStore((s) => s.settings); const disconnect = useReactUiStore((s) => s.disconnect); - const [layoutMode, setLayoutMode] = useState(() => - getInitialLayoutMode(), - ); - useEffect(() => { initialize(); - const media = window.matchMedia("(max-width: 960px)"); - const updateLayout = () => setMobileLayout(media.matches); - updateLayout(); - media.addEventListener("change", updateLayout); - return () => { - media.removeEventListener("change", updateLayout); cleanup(); }; - }, [cleanup, initialize, setMobileLayout]); + }, [cleanup, initialize]); useEffect(() => { const onPopState = () => syncTabFromLocation(); @@ -88,16 +36,6 @@ export function App() { }; }, [syncTabFromLocation]); - useEffect(() => { - persistLayoutMode(layoutMode); - }, [layoutMode]); - - useEffect(() => { - if (layoutMode === "os" && isMobileLayout) { - setLayoutMode("classic"); - } - }, [isMobileLayout, layoutMode]); - useEffect(() => { preloadTabView(tab); }, [tab]); @@ -106,28 +44,20 @@ export function App() { return ; } - if (layoutMode === "os" && !isMobileLayout) { - return ( - - updateSettings({ - theme: settings.theme === "dark" ? "light" : "dark", - }) - } - onDisconnect={disconnect} - onExitOsMode={() => setLayoutMode("classic")} - /> - ); - } - return ( - setLayoutMode("os")} + + updateSettings({ + theme: settings.theme === "dark" ? "light" : "dark", + }) + } + onChangeWallpaper={(wp) => updateSettings({ wallpaper: wp })} + onDisconnect={disconnect} /> ); } @@ -155,7 +85,7 @@ function ConnectScreen() {
- + GSV

GSV

Gateway control UI

@@ -207,138 +137,3 @@ function ConnectScreen() {
); } - -function MainShell({ - osModeAvailable, - onEnableOsMode, -}: { - osModeAvailable: boolean; - onEnableOsMode: () => void; -}) { - const tab = useReactUiStore((s) => s.tab); - const switchTab = useReactUiStore((s) => s.switchTab); - const isMobileLayout = useReactUiStore((s) => s.isMobileLayout); - const navDrawerOpen = useReactUiStore((s) => s.navDrawerOpen); - const toggleNavDrawer = useReactUiStore((s) => s.toggleNavDrawer); - const closeNavDrawer = useReactUiStore((s) => s.closeNavDrawer); - const connectionState = useReactUiStore((s) => s.connectionState); - const updateSettings = useReactUiStore((s) => s.updateSettings); - const settings = useReactUiStore((s) => s.settings); - const disconnect = useReactUiStore((s) => s.disconnect); - - const connectionBadgeVariant = useMemo(() => { - if (connectionState === "connected") { - return "primary"; - } - if (connectionState === "connecting") { - return "outline"; - } - return "destructive"; - }, [connectionState]); - - return ( -
- - ))} -
- ))} -