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..40fdc3b 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, features = ["fullscreen"] } +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..a1d0c13 --- /dev/null +++ b/cli/src/display.rs @@ -0,0 +1,635 @@ +//! 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) +//! + mpsc channel for eval results (main → tokio) + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::mpsc; + +use tao::{ + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowBuilder, WindowId}, +}; +use wry::{WebContext, 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, + /// Browser profile ID (derived from URL origin). When set, + /// wry uses a persistent data directory for this profile. + profile_id: Option, + }, + /// Close an existing surface window. + CloseSurface { surface_id: String }, + /// Update an existing surface (title). + UpdateSurface { + surface_id: String, + label: Option, + }, + /// Execute JavaScript in a webview surface and return the result via IPC. + EvalScript { + surface_id: String, + eval_id: String, + script: String, + }, + /// Shut down the display event loop. + Shutdown, +} + +// ── Eval Result (main thread → tokio) ── + +/// Result of a JavaScript eval, sent from the main thread IPC handler +/// back to the tokio runtime for forwarding to the gateway. +#[derive(Debug, Clone)] +pub struct EvalResult { + pub eval_id: String, + pub surface_id: String, + pub ok: bool, + pub result: Option, + pub error: Option, +} + +// ── 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, + /// Base directory for browser profile storage. + /// Profiles are stored in `{profile_dir}/{profile_id}/`. + pub profile_dir: PathBuf, +} + +impl DisplayHandle { + pub fn open_surface( + &self, + surface_id: String, + url: String, + label: String, + profile_id: Option, + ) { + let _ = self.proxy.send_event(DisplayEvent::OpenSurface { + surface_id, + url, + label, + profile_id, + }); + } + + 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 eval_script(&self, surface_id: String, eval_id: String, script: String) { + let _ = self.proxy.send_event(DisplayEvent::EvalScript { + surface_id, + eval_id, + script, + }); + } + + pub fn shutdown(&self) { + let _ = self.proxy.send_event(DisplayEvent::Shutdown); + } +} + +// ── Constructors ── + +/// Create the display event loop and return a handle for async communication, +/// plus a receiver for eval results flowing from the main thread back to tokio. +/// Call this on the main thread before spawning the tokio runtime. +pub fn create_display( + profile_dir: PathBuf, +) -> ( + DisplayHandle, + EventLoop, + mpsc::Receiver, +) { + let event_loop = EventLoopBuilder::::with_user_event().build(); + let proxy = event_loop.create_proxy(); + let (eval_tx, eval_rx) = mpsc::channel(); + // Store the eval sender in a thread-local so IPC handlers can access it. + // We pass it into run_display_loop instead. + EVAL_RESULT_SENDER.lock().unwrap().replace(eval_tx); + (DisplayHandle { proxy, profile_dir }, event_loop, eval_rx) +} + +/// Global eval result sender. Set once by `create_display`, used by IPC handlers +/// in webviews (which run on the main thread alongside the event loop). +/// Using a Mutex> because wry IPC closures need 'static + Fn. +static EVAL_RESULT_SENDER: std::sync::Mutex>> = + std::sync::Mutex::new(None); + +// ── 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, + /// Browser profile context. Must outlive the WebView. + /// Drop order: webview drops first, then _web_context. + _web_context: Option, +} + +/// 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, profile_dir: PathBuf) -> ! { + 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, + &profile_dir, + ); + } + 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, + profile_dir: &PathBuf, +) { + match event { + DisplayEvent::OpenSurface { + surface_id, + url, + label, + profile_id, + } => { + // 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"; + + // Persistent browser profile: assign a data directory so cookies, + // localStorage, and IndexedDB survive across window close/reopen. + // WebContext must outlive the WebView, so we keep it in an Option + // and store it alongside the window. + let mut web_context_storage: Option = None; + if let Some(ref pid) = profile_id { + let data_dir = profile_dir.join(pid); + if let Err(e) = std::fs::create_dir_all(&data_dir) { + eprintln!( + "[display] Failed to create profile dir {:?}: {}", + data_dir, e + ); + } else { + eprintln!("[display] Using browser profile: {} -> {:?}", pid, data_dir); + web_context_storage = Some(WebContext::new(Some(data_dir))); + } + } + + let builder = if let Some(ref mut ctx) = web_context_storage { + WebViewBuilder::new_with_web_context(ctx) + } else { + WebViewBuilder::new() + }; + + // Clone surface_id for the IPC handler closure + let sid_for_ipc = surface_id.clone(); + + let webview = match builder + .with_url(&url) + .with_user_agent(ua) + .with_autoplay(true) + .with_ipc_handler(move |msg: wry::http::Request| { + // IPC handler: receives JSON messages from JavaScript in the webview. + // Used for returning eval script results. + let body = msg.body(); + handle_ipc_message(&sid_for_ipc, body); + }) + .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, + profile_id + .as_deref() + .map(|p| format!(" (profile: {})", p)) + .unwrap_or_default() + ); + let window_id = window.id(); + window_to_surface.insert(window_id, surface_id.clone()); + surfaces.insert( + surface_id, + SurfaceWindow { + window, + webview, + _web_context: web_context_storage, + }, + ); + } + 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::EvalScript { + surface_id, + eval_id, + script, + } => { + if let Some(sw) = surfaces.get(&surface_id) { + // Two-call eval strategy — no eval() used, CSP/Trusted Types safe. + // + // wry's evaluate_script() bypasses page CSP (engine-level injection), + // but we can't use JS eval() because sites like YouTube enforce Trusted Types. + // + // Call 1 (expression form): wraps the script as `return (SCRIPT)`. + // - Captures expression return values (document.title, Array.from(...), etc.) + // - If the script has semicolons, this fails to parse SILENTLY (no code runs). + // + // Call 2 (statement form): wraps the script as-is in a function body. + // - Always parseable for valid JS. Handles multi-statement scripts. + // - Doesn't capture the last expression's value (returns undefined). + // + // A global guard prevents duplicate IPC responses. Call 1 runs first + // (JS is single-threaded); if it succeeds, Call 2 is a no-op. + let eval_id_json = + serde_json::to_string(&eval_id).unwrap_or_else(|_| format!("\"{}\"", eval_id)); + + // Call 1: expression form — captures return value + let expr_call = format!( + r#"(async () => {{ + if (window.__gsv_ed && window.__gsv_ed[{eid}]) return; + try {{ + const __r = await (async () => {{ return ({script}); }})(); + if (window.__gsv_ed && window.__gsv_ed[{eid}]) return; + window.__gsv_ed = window.__gsv_ed || {{}}; + window.__gsv_ed[{eid}] = true; + window.ipc.postMessage(JSON.stringify({{ + type: "eval_result", evalId: {eid}, ok: true, result: __r + }})); + }} catch (_) {{}} +}})()"#, + script = script, + eid = eval_id_json, + ); + + // Call 2: statement form — always parseable, always responds + let stmt_call = format!( + r#"(async () => {{ + if (window.__gsv_ed && window.__gsv_ed[{eid}]) return; + try {{ + await (async () => {{ {script} }})(); + if (window.__gsv_ed && window.__gsv_ed[{eid}]) return; + window.__gsv_ed = window.__gsv_ed || {{}}; + window.__gsv_ed[{eid}] = true; + window.ipc.postMessage(JSON.stringify({{ + type: "eval_result", evalId: {eid}, ok: true + }})); + }} catch (__e) {{ + if (window.__gsv_ed && window.__gsv_ed[{eid}]) return; + window.__gsv_ed = window.__gsv_ed || {{}}; + window.__gsv_ed[{eid}] = true; + window.ipc.postMessage(JSON.stringify({{ + type: "eval_result", evalId: {eid}, ok: false, error: String(__e) + }})); + }} +}})()"#, + script = script, + eid = eval_id_json, + ); + + let mut dispatched = false; + if let Err(e) = sw.webview.evaluate_script(&expr_call) { + eprintln!( + "[display] Eval expr call failed for surface {}: {}", + surface_id, e + ); + } else { + dispatched = true; + } + if let Err(e) = sw.webview.evaluate_script(&stmt_call) { + eprintln!( + "[display] Eval stmt call failed for surface {}: {}", + surface_id, e + ); + } else { + dispatched = true; + } + + if dispatched { + eprintln!( + "[display] Eval dispatched: {} in surface {}", + eval_id, surface_id + ); + } else { + // Both calls failed at the engine level + if let Ok(guard) = EVAL_RESULT_SENDER.lock() { + if let Some(ref tx) = *guard { + let _ = tx.send(EvalResult { + eval_id, + surface_id, + ok: false, + result: None, + error: Some("Failed to dispatch eval to webview".to_string()), + }); + } + } + } + } else { + eprintln!("[display] Eval failed: surface {} not found", surface_id); + // Send error result back + if let Ok(guard) = EVAL_RESULT_SENDER.lock() { + if let Some(ref tx) = *guard { + let _ = tx.send(EvalResult { + eval_id, + surface_id, + ok: false, + result: None, + error: Some("Surface not found on this display node".to_string()), + }); + } + } + } + } + DisplayEvent::Shutdown => { + eprintln!("[display] Shutdown requested"); + *control_flow = ControlFlow::Exit; + } + } +} + +/// Handle an IPC message from a webview. Called on the main thread. +/// Parses eval result JSON and sends it through the eval result channel. +fn handle_ipc_message(surface_id: &str, body: &str) { + // Parse the JSON message + let msg: serde_json::Value = match serde_json::from_str(body) { + Ok(v) => v, + Err(e) => { + eprintln!( + "[display] IPC parse error from surface {}: {} (body: {})", + surface_id, + e, + &body[..body.len().min(200)] + ); + return; + } + }; + + let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or(""); + + match msg_type { + "eval_result" => { + let eval_id = msg + .get("evalId") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let ok = msg.get("ok").and_then(|v| v.as_bool()).unwrap_or(false); + let result = msg.get("result").map(|v| v.to_string()); + let error = msg + .get("error") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + if eval_id.is_empty() { + eprintln!( + "[display] IPC eval_result missing evalId from surface {}", + surface_id + ); + return; + } + + eprintln!( + "[display] IPC eval result: {} ok={} surface={}", + eval_id, ok, surface_id + ); + + if let Ok(guard) = EVAL_RESULT_SENDER.lock() { + if let Some(ref tx) = *guard { + let _ = tx.send(EvalResult { + eval_id, + surface_id: surface_id.to_string(), + ok, + result, + error, + }); + } + } + } + _ => { + eprintln!( + "[display] Unknown IPC message type '{}' from surface {}", + msg_type, surface_id + ); + } + } +} 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..9779a14 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -64,6 +64,16 @@ 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, + + /// Directory for persistent browser profiles (cookies, localStorage). + /// Default: ~/.gsv/browser-profiles + #[arg(long)] + profile_dir: Option, + /// Node ID (default: hostname) - used as namespace prefix for tools #[arg(long)] id: Option, @@ -799,16 +809,417 @@ 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, + profile_dir, + 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); + let profile_path = profile_dir.clone().unwrap_or_else(|| { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".gsv") + .join("browser-profiles") + }); + return run_node_display_main(&url, token, node_id, workspace_path, profile_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, + profile_dir: PathBuf, +) -> Result<(), Box> { + use gsv::display::{create_display, run_display_loop}; + use gsv::protocol::SurfaceEvalResultPayload; + + let (display_handle, event_loop, eval_result_rx) = create_display(profile_dir.clone()); + + eprintln!( + "[display] Starting display node '{}' -> {} (profiles: {:?})", + node_id, url, profile_dir + ); + + // 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)>(); + + // Bridge: std mpsc (main thread IPC) → tokio mpsc (async runtime). + // Eval results from webview IPC handlers flow from the main thread + // back to the tokio runtime via this channel. + // Convert display::EvalResult → protocol::SurfaceEvalResultPayload. + let (eval_bridge_tx, eval_bridge_rx) = + tokio::sync::mpsc::unbounded_channel::(); + std::thread::spawn(move || { + while let Ok(result) = eval_result_rx.recv() { + let payload = SurfaceEvalResultPayload { + eval_id: result.eval_id, + surface_id: result.surface_id, + ok: result.ok, + result: result.result.and_then(|s| serde_json::from_str(&s).ok()), + error: result.error, + }; + if eval_bridge_tx.send(payload).is_err() { + break; // tokio receiver dropped + } + } + }); + + // 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).await; + } + }); + + if let Err(e) = run_node( + &url_owned, + token, + node_id, + workspace, + true, + Some(surface_tx), + Some(eval_bridge_rx), + ) + .await + { + eprintln!("[display] Node error: {}", e); + display_for_thread.shutdown(); + } + }); + }); + + // Run tao event loop on main thread (blocks forever) + run_display_loop(event_loop, profile_dir); +} + +/// Process a surface event from the gateway and forward to the display handle. +/// For webview surfaces with profiles, handles downloading the R2 snapshot +/// before opening the webview (so the profile data is present on disk). +#[cfg(feature = "display")] +async fn process_surface_event( + display: &gsv::display::DisplayHandle, + gateway_url: &str, + event_name: &str, + payload: Option, +) { + use gsv::display::{gateway_http_url, resolve_surface_url}; + use gsv::protocol::{ + FsAuthorizeResult, SurfaceClosedPayload, SurfaceOpenedPayload, SurfaceUpdatedPayload, + }; + + let Some(payload) = payload else { + return; + }; + + match event_name { + "surface.opened" => { + // Extract the pre-fetched fs read token (if any) before deserializing + let fs_token = payload + .get("fsReadToken") + .and_then(|v| serde_json::from_value::(v.clone()).ok()); + + if let Ok(data) = serde_json::from_value::(payload) { + // If this is a webview with a profile and we have an fs token, + // try to download the R2 snapshot to hydrate the local profile + if let (Some(ref profile_id), Some(ref token_result)) = + (&data.surface.profile_id, &fs_token) + { + let profile_dir = display.profile_dir.join(profile_id); + let snapshot_path = profile_dir.join("__snapshot.tar.gz"); + + // Only download if local profile doesn't exist yet + if !profile_dir.exists() { + let http_base = gateway_http_url(gateway_url); + let snapshot_url = format!( + "{}/fs/browser-profiles/{}/snapshot.tar.gz", + http_base, profile_id + ); + eprintln!( + "[display] Downloading profile snapshot for {} ...", + profile_id + ); + match download_profile_snapshot( + &snapshot_url, + &token_result.token, + &snapshot_path, + ) + .await + { + Ok(true) => { + // Extract the snapshot + if let Err(e) = + extract_profile_snapshot(&snapshot_path, &profile_dir) + { + eprintln!( + "[display] Failed to extract profile {}: {}", + profile_id, e + ); + } + // Clean up the snapshot file + let _ = std::fs::remove_file(&snapshot_path); + eprintln!( + "[display] Profile {} hydrated from R2", + profile_id + ); + } + Ok(false) => { + // No snapshot in R2 — start fresh + eprintln!( + "[display] No R2 snapshot for profile {}, starting fresh", + profile_id + ); + } + Err(e) => { + eprintln!( + "[display] Profile download failed for {}: {}", + profile_id, e + ); + } + } + } + } + + 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, + data.surface.profile_id, + ); + } + } + "surface.closed" => { + // Extract the pre-fetched fs write token before deserializing + let fs_write_token = payload + .get("fsWriteToken") + .and_then(|v| serde_json::from_value::(v.clone()).ok()); + + if let Ok(data) = serde_json::from_value::(payload) { + // Close the webview window first + display.close_surface(data.surface_id); + + // Upload profile snapshot to R2 if this surface had a profile + if let (Some(ref profile_id), Some(ref token_result)) = + (&data.profile_id, &fs_write_token) + { + let profile_dir = display.profile_dir.join(profile_id); + if profile_dir.exists() { + let http_base = gateway_http_url(gateway_url); + let upload_url = format!( + "{}/fs/browser-profiles/{}/snapshot.tar.gz", + http_base, profile_id + ); + + // Small delay to let the webview flush its data + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + eprintln!( + "[display] Uploading profile snapshot for {} ...", + profile_id + ); + match create_and_upload_profile( + &profile_dir, + &upload_url, + &token_result.token, + ) + .await + { + Ok(()) => { + eprintln!( + "[display] Profile {} uploaded to R2", + profile_id + ); + } + Err(e) => { + eprintln!( + "[display] Profile upload failed for {}: {}", + profile_id, e + ); + } + } + } + } + } + } + "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); + } + } + "surface.eval" => { + // Forward eval request to the display's webview for JS execution. + // The result comes back via the IPC handler → eval result channel. + if let Ok(data) = + serde_json::from_value::(payload) + { + display.eval_script(data.surface_id, data.eval_id, data.script); + } + } + _ => {} + } +} + +/// Download a profile snapshot from the /fs/ endpoint. +/// Returns Ok(true) if downloaded, Ok(false) if 404 (no snapshot), Err on failure. +#[cfg(feature = "display")] +async fn download_profile_snapshot( + url: &str, + token: &str, + dest: &std::path::Path, +) -> Result> { + let client = reqwest::Client::new(); + let resp = client + .get(url) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await?; + + if resp.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(false); + } + if !resp.status().is_success() { + return Err(format!("HTTP {}", resp.status()).into()); + } + + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + let bytes = resp.bytes().await?; + std::fs::write(dest, &bytes)?; + Ok(true) +} + +/// Extract a tar.gz snapshot into the profile directory. +#[cfg(feature = "display")] +fn extract_profile_snapshot( + archive_path: &std::path::Path, + dest_dir: &std::path::Path, +) -> Result<(), Box> { + let file = std::fs::File::open(archive_path)?; + let gz = flate2::read::GzDecoder::new(file); + let mut archive = tar::Archive::new(gz); + std::fs::create_dir_all(dest_dir)?; + archive.unpack(dest_dir)?; + Ok(()) +} + +/// Create a tar.gz snapshot of a profile directory and upload it to R2 via /fs/. +#[cfg(feature = "display")] +async fn create_and_upload_profile( + profile_dir: &std::path::Path, + upload_url: &str, + token: &str, +) -> Result<(), Box> { + // Create tar.gz in memory + let tar_gz_bytes = tokio::task::spawn_blocking({ + let profile_dir = profile_dir.to_path_buf(); + move || -> Result, String> { + let mut buf = Vec::new(); + { + let enc = flate2::write::GzEncoder::new(&mut buf, flate2::Compression::fast()); + let mut tar_builder = tar::Builder::new(enc); + tar_builder + .append_dir_all(".", &profile_dir) + .map_err(|e| format!("tar: {}", e))?; + let enc = tar_builder.into_inner().map_err(|e| format!("tar finish: {}", e))?; + enc.finish().map_err(|e| format!("gzip: {}", e))?; + } + Ok(buf) + } + }) + .await + .map_err(|e| format!("spawn_blocking: {}", e))? + .map_err(|e| -> Box { e.into() })?; + + let size_mb = tar_gz_bytes.len() as f64 / (1024.0 * 1024.0); + if tar_gz_bytes.len() > 50 * 1024 * 1024 { + return Err(format!( + "Profile snapshot too large ({:.1} MB, limit 50 MB)", + size_mb + ) + .into()); + } + eprintln!( + "[display] Profile snapshot: {:.1} MB compressed", + size_mb + ); + + // Upload via HTTP PUT + let client = reqwest::Client::new(); + let resp = client + .put(upload_url) + .header("Authorization", format!("Bearer {}", token)) + .header("Content-Type", "application/gzip") + .header( + "X-R2-Meta", + serde_json::json!({ + "version": "1", + "uploadedAt": chrono::Utc::now().to_rfc3339(), + }) + .to_string(), + ) + .body(tar_gz_bytes) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(format!("Upload failed: HTTP {} - {}", status, body).into()); + } + Ok(()) +} + +async fn async_main_with(cli: Cli) -> Result<(), Box> { // Load config from file let cfg = CliConfig::load(); @@ -831,10 +1242,21 @@ async fn async_main() -> Result<(), Box> { } Commands::Node { foreground, + display, + profile_dir: _, 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 +1272,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, None).await } else { run_node_default_managed( &cfg, @@ -3077,6 +3499,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 +3534,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 +3695,9 @@ async fn run_node( token: Option, node_id: String, workspace: PathBuf, + display_mode: bool, + surface_tx: Option)>>, + eval_result_rx: Option>, ) -> Result<(), Box> { let logger = NodeLogger::new(&node_id, &workspace)?; let log_path = logger::node_log_path()?; @@ -3319,6 +3750,73 @@ async fn run_node( const INITIAL_RETRY_DELAY: tokio::time::Duration = tokio::time::Duration::from_secs(3); const MAX_RETRY_DELAY: tokio::time::Duration = tokio::time::Duration::from_secs(300); let mut retry_delay = INITIAL_RETRY_DELAY; + // Shared "current connection" reference for the eval result forwarder. + // Updated on each reconnect so the forwarder always uses the latest connection. + let current_conn: Arc>>> = + Arc::new(tokio::sync::Mutex::new(None)); + + // Spawn eval result forwarder task (runs for the lifetime of the node). + // Reads eval results from the display IPC bridge and sends them back + // to the gateway as surface.eval.result RPCs. + let _eval_result_task = if let Some(mut rx) = eval_result_rx { + let conn_ref = current_conn.clone(); + let logger_for_eval = logger.clone(); + Some(tokio::spawn(async move { + while let Some(result) = rx.recv().await { + let params = serde_json::to_value(&result).unwrap_or_default(); + let conn_guard = conn_ref.lock().await; + let Some(ref conn) = *conn_guard else { + logger_for_eval.warn( + "surface.eval.result.no_conn", + json!({ "evalId": result.eval_id }), + ); + continue; + }; + let conn = conn.clone(); + drop(conn_guard); // Release lock before awaiting RPC + + match conn + .request_with_timeout( + "surface.eval.result", + Some(params), + std::time::Duration::from_secs(10), + ) + .await + { + Ok(res) if res.ok => { + logger_for_eval.info( + "surface.eval.result.sent", + json!({ + "evalId": result.eval_id, + "surfaceId": result.surface_id, + "ok": result.ok, + }), + ); + } + Ok(res) => { + logger_for_eval.warn( + "surface.eval.result.failed", + json!({ + "evalId": result.eval_id, + "error": res.error, + }), + ); + } + Err(e) => { + logger_for_eval.warn( + "surface.eval.result.send_error", + json!({ + "evalId": result.eval_id, + "error": e.to_string(), + }), + ); + } + } + } + })) + } else { + None + }; loop { logger.info("connect.attempt", json!({ "url": url })); @@ -3326,13 +3824,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 +3894,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 +3902,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,12 +4160,158 @@ async fn run_node( } } } + } else if evt.event.starts_with("surface.") { + // Forward surface events to display module (if active). + // For surface.opened with a profileId, pre-fetch an fs read + // token so the display task can download the profile snapshot. + if let Some(ref tx) = surface_tx { + let event_name = evt.event.clone(); + let mut extra = serde_json::Map::new(); + + // Pre-fetch fs tokens for profile sync: + // - surface.opened: read token (download snapshot) + // - surface.closed: write token (upload snapshot) + + if event_name == "surface.closed" { + if let Some(ref payload_val) = evt.payload { + if let Some(profile_id) = payload_val + .get("profileId") + .and_then(|v| v.as_str()) + { + let prefix = format!( + "browser-profiles/{}/", + profile_id + ); + let params = json!({ + "pathPrefix": prefix, + "mode": "write" + }); + match conn + .request_with_timeout( + "fs.authorize", + Some(params), + std::time::Duration::from_secs(5), + ) + .await + { + Ok(res) if res.ok => { + if let Some(payload) = res.payload { + extra.insert( + "fsWriteToken".to_string(), + payload, + ); + } + } + Ok(res) => { + logger.warn( + "surface.fs_authorize_write.failed", + json!({ + "error": res.error, + "profileId": profile_id, + }), + ); + } + Err(e) => { + logger.warn( + "surface.fs_authorize_write.error", + json!({ + "error": e.to_string(), + "profileId": profile_id, + }), + ); + } + } + } + } + } + + if event_name == "surface.opened" { + if let Some(ref payload_val) = evt.payload { + if let Some(profile_id) = payload_val + .get("surface") + .and_then(|s| s.get("profileId")) + .and_then(|v| v.as_str()) + { + let prefix = format!( + "browser-profiles/{}/", + profile_id + ); + let params = json!({ + "pathPrefix": prefix, + "mode": "read" + }); + match conn + .request_with_timeout( + "fs.authorize", + Some(params), + std::time::Duration::from_secs(5), + ) + .await + { + Ok(res) if res.ok => { + if let Some(payload) = res.payload { + extra.insert( + "fsReadToken".to_string(), + payload, + ); + } + } + Ok(res) => { + logger.warn( + "surface.fs_authorize.failed", + json!({ + "error": res.error, + "profileId": profile_id, + }), + ); + } + Err(e) => { + logger.warn( + "surface.fs_authorize.error", + json!({ + "error": e.to_string(), + "profileId": profile_id, + }), + ); + } + } + } + } + } + + let send_val = if extra.is_empty() { + evt.payload.clone() + } else { + // Merge extra fields into the payload + let mut merged = evt + .payload + .as_ref() + .and_then(|v| v.as_object().cloned()) + .unwrap_or_default(); + merged.extend(extra); + Some(serde_json::Value::Object(merged)) + }; + + if let Err(e) = tx.send((event_name.clone(), send_val)) { + logger.warn( + "surface.event.forward_failed", + json!({ + "event": event_name, + "error": e.to_string(), + }), + ); + } + } } } }); }) .await; + // Update the shared connection reference so the eval result forwarder + // uses the latest connection after each reconnect. + *current_conn.lock().await = Some(conn.clone()); + let flushed = flush_exec_event_outbox(&conn, &exec_event_outbox, &logger).await; if flushed > 0 { logger.info( diff --git a/cli/src/protocol.rs b/cli/src/protocol.rs index 77884c4..147ff9b 100644 --- a/cli/src/protocol.rs +++ b/cli/src/protocol.rs @@ -230,6 +230,153 @@ 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 SurfaceProfileLock { + pub node_id: String, + pub surface_id: String, +} + +#[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, + // Browser profile persistence + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_lock: Option, +} + +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceUpdatedPayload { + pub surface: Surface, +} + +// ── Surface Eval Protocol ── + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceEvalRequestPayload { + pub eval_id: String, + pub surface_id: String, + pub script: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SurfaceEvalResultPayload { + pub eval_id: String, + pub surface_id: String, + pub ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +// ── Filesystem Token Protocol ── + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsAuthorizeParams { + pub path_prefix: String, + pub mode: String, // "read" | "write" +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsAuthorizeResult { + pub token: String, + pub expires_at: f64, + pub path_prefix: String, +} + impl RequestFrame { pub fn new(method: &str, params: Option) -> Self { Self { diff --git a/gateway/package-lock.json b/gateway/package-lock.json index 977c432..85562b5 100644 --- a/gateway/package-lock.json +++ b/gateway/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "@clack/prompts": "^1.0.0", - "@mariozechner/pi-ai": "^0.54.0", + "@mariozechner/pi-ai": "^0.55.1", + "agents": "^0.5.1", "picocolors": "^1.1.1", "ws": "^8.19.0" }, @@ -23,6 +24,55 @@ "wrangler": "^4.61.1" } }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.53", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.53.tgz", + "integrity": "sha512-QT3FEoNARMRlk8JJVR7L98exiK9C8AGfrEJVbRxBT1yIXKs/N19o/+PsjTRVsARgDJNcy9JbJp1FspKucEat0Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.15", + "@vercel/oidc": "3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.15.tgz", + "integrity": "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@anthropic-ai/sdk": { "version": "0.73.0", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", @@ -43,6 +93,23 @@ } } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -1125,6 +1192,24 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", + "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, "node_modules/@clack/core": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz", @@ -2421,7 +2506,6 @@ "version": "4.20260131.0", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260131.0.tgz", "integrity": "sha512-ELgvb2mp68Al50p+FmpgCO2hgU5o4tmz8pi7kShN+cRXc0UZoEdxpDIikR0CeT7b3tV7wlnEnsUzd0UoJLS0oQ==", - "dev": true, "license": "MIT OR Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { @@ -2913,6 +2997,18 @@ } } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -3455,10 +3551,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@mariozechner/pi-ai": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.54.0.tgz", - "integrity": "sha512-XHhMIbFFHCa4mbiYdttfhVg6r3VmFD5tAiW4tjnmf33FhLUCRd76bGMQRc4kLWXPKCi/U4nqAErvaGiZUY4B8A==", + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.55.1.tgz", + "integrity": "sha512-JJX1LrVWPUPMExu0f89XR4nMNP37+FNLjEE4cIHq9Hi6xQtOiiEi7OjDFMx58hWsq81xH1CwmQXqGTWBjbXKTw==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -3509,6 +3611,46 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@octokit/auth-token": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", @@ -3709,6 +3851,16 @@ "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4858,6 +5010,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT", + "peer": true + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -4899,6 +5058,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", @@ -4908,6 +5079,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@vercel/oidc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", + "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 20" + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -5023,6 +5204,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -5055,6 +5249,99 @@ "node": ">= 14" } }, + "node_modules/agents": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/agents/-/agents-0.5.1.tgz", + "integrity": "sha512-Rx7KXpArykLdsPcSzNGtF3uARrQsn9DY5hDGqD9Pl3FrK/aZ6Z6svXb5FhGZPOapXMgf+DyEDY/NPPHNsVLNag==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.1.1", + "@modelcontextprotocol/sdk": "1.26.0", + "cron-schedule": "^6.0.0", + "json-schema": "^0.4.0", + "json-schema-to-typescript": "^15.0.4", + "mimetext": "^3.0.28", + "nanoid": "^5.1.6", + "partyserver": "^0.2.0", + "partysocket": "1.1.14", + "yargs": "^18.0.0" + }, + "bin": { + "agents": "dist/cli/index.js" + }, + "peerDependencies": { + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/react": "^3.0.0", + "@cloudflare/ai-chat": "^0.1.3", + "@cloudflare/codemode": "^0.1.0", + "@x402/core": "^2.0.0", + "@x402/evm": "^2.0.0", + "ai": "^6.0.0", + "react": "^19.0.0", + "viem": ">=2.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@ai-sdk/openai": { + "optional": true + }, + "@ai-sdk/react": { + "optional": true + }, + "@cloudflare/ai-chat": { + "optional": true + }, + "@cloudflare/codemode": { + "optional": true + }, + "@x402/core": { + "optional": true + }, + "@x402/evm": { + "optional": true + }, + "viem": { + "optional": true + } + } + }, + "node_modules/agents/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/ai": { + "version": "6.0.97", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.97.tgz", + "integrity": "sha512-eZIAcBymwGhBwncRH/v9pillZNMeRCDkc4BwcvwXerXd7sxjVxRis3ZNCNCpP02pVH4NLs81ljm4cElC4vbNcQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@ai-sdk/gateway": "3.0.53", + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.15", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -5740,6 +6027,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -5837,6 +6130,30 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/bowser": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", @@ -5884,6 +6201,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5894,6 +6220,35 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -5940,6 +6295,60 @@ "dev": true, "license": "MIT" }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -5993,6 +6402,28 @@ "node": ">=18" } }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -6007,6 +6438,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-js-pure": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", + "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6014,6 +6465,32 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cron-schedule": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cron-schedule/-/cron-schedule-6.0.0.tgz", + "integrity": "sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6128,10 +6605,19 @@ "node": ">= 14" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6271,6 +6757,20 @@ } } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6286,12 +6786,27 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -6315,6 +6830,24 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -6322,6 +6855,18 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", @@ -6364,6 +6909,21 @@ "@esbuild/win32-x64": "0.27.0" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escodegen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", @@ -6426,6 +6986,42 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-polyfill": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", + "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==", + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", @@ -6476,6 +7072,85 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -6557,7 +7232,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -6610,6 +7284,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-process": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-process/-/find-process-2.0.0.tgz", @@ -6699,6 +7394,24 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6714,6 +7427,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gaxios": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", @@ -6743,6 +7465,64 @@ "node": ">=18" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -6850,6 +7630,18 @@ "node": ">=14" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6880,6 +7672,59 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", + "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6916,6 +7761,22 @@ "node": ">=18.18.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -6927,7 +7788,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ip-address": { @@ -6939,6 +7799,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", @@ -6962,6 +7831,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -6971,6 +7849,18 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -7015,6 +7905,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", @@ -7085,6 +7981,21 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -7092,6 +8003,18 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -7101,6 +8024,12 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-to-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", @@ -7114,12 +8043,41 @@ "node": ">=16" } }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -7191,6 +8149,12 @@ "immediate": "~3.0.5" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -7233,21 +8197,113 @@ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimetext": { + "version": "3.0.28", + "resolved": "https://registry.npmjs.org/mimetext/-/mimetext-3.0.28.tgz", + "integrity": "sha512-eQXpbNrtxLCjUtiVbR/qR09dbPgZ2o+KR1uA7QKqGhbn8QV7HIL16mXXsobBL4/8TqoYh1us31kfz+dNfCev9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@babel/runtime-corejs3": "^7.26.0", + "js-base64": "^3.7.7", + "mime-types": "^2.1.35" + }, + "funding": { + "type": "patreon", + "url": "https://patreon.com/muratgozel" + } + }, + "node_modules/mimetext/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, + "node_modules/mimetext/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "bin": { - "mime": "cli.js" + "dependencies": { + "mime-db": "1.52.0" }, "engines": { - "node": ">=10.0.0" + "node": ">= 0.6" } }, "node_modules/miniflare": { @@ -7308,6 +8364,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -7342,6 +8407,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -7432,6 +8506,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -7439,6 +8534,27 @@ "dev": true, "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -7559,12 +8675,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", "license": "MIT" }, + "node_modules/partyserver": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.2.0.tgz", + "integrity": "sha512-yjaReXbTjMIH0zzWshtA3Um+A3BGYYRZWg0AzTzCIlxL4oDLLf/1og9xhhm1lAMFe52uej8QEwJzACnCy9s5ww==", + "license": "ISC", + "dependencies": { + "nanoid": "^5.1.6" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20240729.0" + } + }, + "node_modules/partyserver/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/partysocket": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.14.tgz", + "integrity": "sha512-QcmlVPfNL55BZ8jETbX0VdKgBniYKbWB9Kma0ksE0gNRgXy9Q/lNn2T8iOBR4qpZHJJvS1jleI5W7MKmg+ynzQ==", + "license": "MIT", + "dependencies": { + "event-target-polyfill": "^0.0.4" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7630,7 +8794,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7639,6 +8802,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -7668,6 +8840,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -7734,6 +8921,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", @@ -7759,6 +8959,55 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -7871,6 +9120,32 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -7904,6 +9179,12 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -7917,6 +9198,51 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -7924,6 +9250,12 @@ "dev": true, "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -7990,6 +9322,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -8090,6 +9494,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -8290,7 +9703,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -8333,6 +9745,15 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -8365,6 +9786,20 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -8432,6 +9867,15 @@ "dev": true, "license": "ISC" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8439,6 +9883,15 @@ "dev": true, "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -8798,6 +10251,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -8835,6 +10294,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", @@ -8851,6 +10319,55 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", @@ -8894,7 +10411,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/gateway/package.json b/gateway/package.json index 860145b..1963fc9 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", @@ -29,7 +25,8 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@mariozechner/pi-ai": "^0.54.0", + "@mariozechner/pi-ai": "^0.55.1", + "agents": "^0.5.1", "picocolors": "^1.1.1", "ws": "^8.19.0" } diff --git a/gateway/src/agents/tools/constants.ts b/gateway/src/agents/tools/constants.ts index 33cf6e1..3c617c8 100644 --- a/gateway/src/agents/tools/constants.ts +++ b/gateway/src/agents/tools/constants.ts @@ -11,4 +11,5 @@ export const NATIVE_TOOLS = { MESSAGE: `${NATIVE_TOOL_PREFIX}Message`, SESSIONS_LIST: `${NATIVE_TOOL_PREFIX}SessionsList`, SESSION_SEND: `${NATIVE_TOOL_PREFIX}SessionSend`, + VIEW: `${NATIVE_TOOL_PREFIX}View`, } as const; diff --git a/gateway/src/agents/tools/index.ts b/gateway/src/agents/tools/index.ts index 777b8ed..f2ab1da 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,18 +59,21 @@ export function getNativeToolDefinitions(): ToolDefinition[] { ...getMessageToolDefinitions(), ...getSessionsToolDefinitions(), ...getTransferToolDefinitions(), + ...getSurfaceToolDefinitions(), ]; } /** * Execute a native tool - * Returns { ok, result?, error? } + * Returns { ok, result?, error?, deferred? } */ export async function executeNativeTool( context: { bucket: R2Bucket; agentId: string; gateway?: NativeToolExecutionContext["gateway"]; + callId?: string; + sessionKey?: string; }, toolName: string, args: Record, diff --git a/gateway/src/agents/tools/surface.ts b/gateway/src/agents/tools/surface.ts new file mode 100644 index 0000000..fefaf90 --- /dev/null +++ b/gateway/src/agents/tools/surface.ts @@ -0,0 +1,182 @@ +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.VIEW, + description: + "Manage views (surfaces) on connected clients and display nodes.\n\n" + + "Actions:\n" + + " open — Open a view. Requires kind and contentRef.\n" + + " kind=app opens a built-in tab (chat, sessions, channels, nodes, workspace, cron, logs, settings, overview).\n" + + " kind=media opens a media player (contentRef = URL).\n" + + " kind=webview opens an arbitrary URL in a native browser window.\n" + + " If targetClientId is omitted, webview/media auto-target display nodes; app auto-targets web clients.\n" + + " list — List all open views. Optional targetClientId filter.\n" + + " close — Close a view by surfaceId.\n" + + " eval — Execute JavaScript in a webview surface (like DevTools console). Requires surfaceId and script.\n" + + " Returns the JSON-serializable result. Only works on kind=webview surfaces on display nodes.", + inputSchema: { + type: "object", + properties: { + action: { + type: "string", + enum: ["open", "list", "close", "eval"], + description: "The operation to perform.", + }, + kind: { + type: "string", + enum: ["app", "media", "webview"], + description: "Type of view to open. Required for action=open.", + }, + contentRef: { + type: "string", + description: + "What to display. Required for action=open. " + + "For kind=app: tab name (e.g. 'chat'). For kind=media or kind=webview: a URL.", + }, + label: { + type: "string", + description: "Window title. Defaults to contentRef. Used with action=open.", + }, + targetClientId: { + type: "string", + description: + "Target client or node ID. Used with action=open (optional auto-target) and action=list (optional filter).", + }, + surfaceId: { + type: "string", + description: "The surfaceId of an existing view. Required for action=close and action=eval.", + }, + script: { + type: "string", + description: + "JavaScript to execute in the webview. Required for action=eval. " + + "The return value of the last expression is JSON-serialized and returned.", + }, + }, + required: ["action"], + }, + }, +]; + +export const surfaceNativeToolHandlers: NativeToolHandlerMap = { + [NATIVE_TOOLS.VIEW]: async (context, args) => { + if (!context.gateway) { + return { ok: false, error: "View tool unavailable: gateway context missing" }; + } + + const action = typeof args.action === "string" ? args.action : ""; + + switch (action) { + case "open": { + 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 for action=open" }; + } + + 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, + }, + }; + } + + case "list": { + 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, + })), + }, + }; + } + + case "close": { + const surfaceId = typeof args.surfaceId === "string" ? args.surfaceId : ""; + if (!surfaceId) { + return { ok: false, error: "surfaceId is required for action=close" }; + } + + 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 } }; + } + + case "eval": { + const surfaceId = typeof args.surfaceId === "string" ? args.surfaceId : ""; + const script = typeof args.script === "string" ? args.script : ""; + + if (!surfaceId) { + return { ok: false, error: "surfaceId is required for action=eval" }; + } + if (!script) { + return { ok: false, error: "script is required for action=eval" }; + } + + if (!context.callId || !context.sessionKey) { + return { ok: false, error: "eval requires callId and sessionKey in execution context" }; + } + + // Fire-and-forget: pass callId/sessionKey so the result routes back via Session DO toolResult() + const result = (await context.gateway.evalSurface( + surfaceId, + script, + context.callId, + context.sessionKey, + )) as unknown as { ok: boolean; error?: string; evalId?: string }; + + if (!result.ok) { + return { ok: false, error: result.error }; + } + + // Return deferred — the actual result arrives asynchronously via toolResult() + return { ok: true, deferred: true }; + } + + default: + return { ok: false, error: `Unknown action: "${action}". Use open, list, close, or eval.` }; + } + }, +}; diff --git a/gateway/src/agents/tools/types.ts b/gateway/src/agents/tools/types.ts index a46bbfc..f489e0f 100644 --- a/gateway/src/agents/tools/types.ts +++ b/gateway/src/agents/tools/types.ts @@ -1,12 +1,14 @@ import type { Gateway } from "../../gateway/do"; -export type NativeToolResult = { ok: boolean; result?: unknown; error?: string }; +export type NativeToolResult = { ok: boolean; result?: unknown; error?: string; deferred?: boolean }; export type NativeToolExecutionContext = { bucket: R2Bucket; agentId: string; basePath: string; gateway?: DurableObjectStub; + callId?: string; + sessionKey?: string; }; export type NativeToolHandler = ( diff --git a/gateway/src/gateway/do.ts b/gateway/src/gateway/do.ts index 3abc018..caa1997 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,28 @@ export class Gateway extends DurableObject { }, ); + // Surface registry — renderable views across all clients + readonly surfaces = PersistedObject>( + this.ctx.storage.kv, + { prefix: "surfaces:" }, + ); + + // ── Filesystem token store (in-memory, ephemeral) ── + // Short-lived tokens for authenticated R2 access via /fs/ endpoint. + private fsTokens = new Map(); + + // ── Browser profile locks (in-memory) ── + // Tracks which node currently holds a profile open. Key = profileId. + private profileLocks = new Map(); + + // ── Pending surface eval calls ── + // Key: evalId, Value: { ws, frameId } for deferred RPC response (WS callers). + readonly pendingEvals = new Map(); + // Key: evalId, Value: routing info for agent tool calls (fire-and-forget, routed back via Session DO). + readonly pendingEvalRoutes = PersistedObject< + Record + >(this.ctx.storage.kv, { prefix: "pendingEvalRoutes:" }); + // Heartbeat scheduler state (persisted to survive DO eviction) readonly heartbeatScheduler: { initialized: boolean } = PersistedObject<{ initialized: boolean; @@ -345,6 +368,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 +385,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 +854,76 @@ 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) { + if (surface.profileId) { + this.releaseProfileLock(surface.profileId, surfaceId); + } + delete this.surfaces[surfaceId]; + } + } + // Also release any remaining profile locks for this node + this.releaseProfileLocksForNode(clientId); + } + broadcastToSession(sessionKey: string, payload: ChatEventPayload): void { const evt: EventFrame = { type: "evt", @@ -894,6 +990,314 @@ 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(); + + // Browser profile handling for webview surfaces + let profileId: string | undefined; + let profileVersion: number | undefined; + let profileLock: Surface["profileLock"] | undefined; + if (params.kind === "webview" && params.contentRef) { + profileId = Gateway.deriveProfileId(params.contentRef) ?? undefined; + if (profileId) { + // Check if target is a node (profile persistence only works on nodes) + const isNode = this.nodes.has(targetClientId); + if (isNode) { + if (!this.acquireProfileLock(profileId, targetClientId, surfaceId)) { + const holder = this.profileLocks.get(profileId); + return { + ok: false, + error: `Profile "${profileId}" is locked by node "${holder?.nodeId}"`, + }; + } + profileLock = { nodeId: targetClientId, surfaceId }; + } + // Check if a snapshot exists in R2 + const snapshotKey = await this.getProfileSnapshotKey(profileId); + if (snapshotKey) { + // profileVersion is stored in surface metadata; for now read from R2 custom metadata + const head = await this.env.STORAGE.head(snapshotKey); + profileVersion = head?.customMetadata?.version + ? parseInt(head.customMetadata.version, 10) + : 1; + } + } + } + + 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, + profileId, + profileVersion, + profileLock, + }; + + 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}` + + (profileId ? ` profile=${profileId}` : ""), + ); + + 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}` }; + } + + // Release browser profile lock if held + if (surface.profileId) { + this.releaseProfileLock(surface.profileId, surfaceId); + } + + const targetClientId = surface.targetClientId; + const profileId = surface.profileId; + delete this.surfaces[surfaceId]; + + this.broadcastSurfaceEvent( + "surface.closed", + { surfaceId, targetClientId, profileId }, + targetClientId, + ); + + console.log(`[Gateway] Surface closed via tool: ${surfaceId}`); + return { ok: true }; + } + + /** + * Execute JavaScript in a webview surface (fire-and-forget). + * Called by Session DO via gateway stub (for agent tools like gsv__View eval). + * Sends the script to the target node and returns immediately. + * The result is routed back asynchronously via handleSurfaceEvalResult → Session DO toolResult(). + */ + async evalSurface( + surfaceId: string, + script: string, + callId?: string, + sessionKey?: string, + ): Promise<{ ok: boolean; error?: string; evalId?: string }> { + const surface = this.surfaces[surfaceId]; + if (!surface) { + return { ok: false, error: `Surface not found: ${surfaceId}` }; + } + if (surface.kind !== "webview") { + return { ok: false, error: `Surface kind "${surface.kind}" does not support eval` }; + } + + const targetClientId = surface.targetClientId; + if (!this.nodes.has(targetClientId)) { + return { ok: false, error: "Target is not a display node" }; + } + const targetWs = this.nodes.get(targetClientId); + if (!targetWs || targetWs.readyState !== WebSocket.OPEN) { + return { ok: false, error: `Target node "${targetClientId}" not connected` }; + } + + const evalId = crypto.randomUUID(); + + // Store routing info so the result handler can route back to Session DO + if (callId && sessionKey) { + this.pendingEvalRoutes[evalId] = { + sessionKey, + callId, + createdAt: Date.now(), + }; + } + + // Send the eval request to the target node + const payload: import("../protocol/surface").SurfaceEvalRequestPayload = { + evalId, + surfaceId, + script, + }; + targetWs.send(JSON.stringify({ type: "evt", event: "surface.eval", payload })); + + console.log(`[Gateway] Surface eval dispatched via tool: ${evalId} -> surface ${surfaceId}`); + + return { ok: true, evalId }; + } + + // ---- Filesystem Token System ---- + + /** + * Issue a short-lived token for R2 access via the /fs/ endpoint. + * Called by the fs.authorize RPC handler. + */ + authorizeFs(pathPrefix: string, mode: import("../protocol/fs").FsMode): import("../protocol/fs").FsAuthorizeResult { + // Lazy GC: prune expired tokens + const now = Date.now(); + for (const [tok, entry] of this.fsTokens) { + if (entry.expiresAt <= now) this.fsTokens.delete(tok); + } + + const token = crypto.randomUUID(); + const expiresAt = now + 60_000; // 60 seconds + this.fsTokens.set(token, { pathPrefix, mode, expiresAt }); + + return { token, expiresAt, pathPrefix }; + } + + /** + * Verify a token for a given path and mode. + * Called by the worker fetch handler before proxying R2 operations. + * Returns true if the token is valid and grants access to the requested path. + */ + verifyFsToken(token: string, path: string, mode: import("../protocol/fs").FsMode): boolean { + const entry = this.fsTokens.get(token); + if (!entry) return false; + + // Check expiry + if (entry.expiresAt <= Date.now()) { + this.fsTokens.delete(token); + return false; + } + + // Check mode + if (entry.mode !== mode) return false; + + // Check path prefix + if (!path.startsWith(entry.pathPrefix)) return false; + + return true; + } + + // ---- Browser Profile Locks ---- + + /** + * Derive a profileId from a URL origin. + * e.g. "https://github.com/settings" → "github.com" + */ + static deriveProfileId(urlStr: string): string | null { + try { + const u = new URL(urlStr); + return u.hostname; + } catch { + return null; + } + } + + /** + * Acquire a profile lock for a node. Returns false if already locked by another node. + */ + acquireProfileLock(profileId: string, nodeId: string, surfaceId: string): boolean { + const existing = this.profileLocks.get(profileId); + if (existing && existing.nodeId !== nodeId) { + return false; // locked by another node + } + this.profileLocks.set(profileId, { nodeId, surfaceId }); + return true; + } + + /** + * Release a profile lock. Only releases if the lock is held by the specified surface. + */ + releaseProfileLock(profileId: string, surfaceId?: string): void { + const existing = this.profileLocks.get(profileId); + if (!existing) return; + if (surfaceId && existing.surfaceId !== surfaceId) return; + this.profileLocks.delete(profileId); + } + + /** + * Release all profile locks held by a given node (on disconnect). + */ + releaseProfileLocksForNode(nodeId: string): void { + for (const [profileId, lock] of this.profileLocks) { + if (lock.nodeId === nodeId) { + this.profileLocks.delete(profileId); + } + } + } + + /** + * Get the current R2 snapshot path for a profile, if one has been uploaded. + * Returns the R2 key or null. + */ + async getProfileSnapshotKey(profileId: string): Promise { + const key = `browser-profiles/${profileId}/snapshot.tar.gz`; + const head = await this.env.STORAGE.head(key); + return head ? key : null; + } + // ---- Heartbeat System ---- private getGatewayAlarmParticipants( diff --git a/gateway/src/gateway/rpc-handlers/connect.ts b/gateway/src/gateway/rpc-handlers/connect.ts index 124500b..d637589 100644 --- a/gateway/src/gateway/rpc-handlers/connect.ts +++ b/gateway/src/gateway/rpc-handlers/connect.ts @@ -156,6 +156,14 @@ export const handleConnect: Handler<"connect"> = async (ctx) => { "channel.login", "channel.logout", "channels.list", + "surface.open", + "surface.close", + "surface.update", + "surface.focus", + "surface.list", + "surface.eval", + "surface.eval.result", + "fs.authorize", ], events: [ "chat", @@ -163,6 +171,11 @@ export const handleConnect: Handler<"connect"> = async (ctx) => { "tool.result", "logs.get", "channel.outbound", + "surface.opened", + "surface.closed", + "surface.updated", + "surface.eval", + "surface.eval.result", ], }, }; diff --git a/gateway/src/gateway/rpc-handlers/fs.ts b/gateway/src/gateway/rpc-handlers/fs.ts new file mode 100644 index 0000000..52c11b5 --- /dev/null +++ b/gateway/src/gateway/rpc-handlers/fs.ts @@ -0,0 +1,19 @@ +import type { Handler } from "../../protocol/methods"; +import { RpcError } from "../../shared/utils"; + +export const handleFsAuthorize: Handler<"fs.authorize"> = ({ gw, params }) => { + if (!params?.pathPrefix || typeof params.pathPrefix !== "string") { + throw new RpcError(400, "pathPrefix is required"); + } + if (params.mode !== "read" && params.mode !== "write") { + throw new RpcError(400, "mode must be 'read' or 'write'"); + } + + // Sanitize: no path traversal, no leading slash + const cleaned = params.pathPrefix.replace(/^\/+/, "").replace(/\.\.\//g, ""); + if (!cleaned) { + throw new RpcError(400, "pathPrefix is empty after sanitization"); + } + + return gw.authorizeFs(cleaned, params.mode); +}; diff --git a/gateway/src/gateway/rpc-handlers/index.ts b/gateway/src/gateway/rpc-handlers/index.ts index 9f18e10..40781a2 100644 --- a/gateway/src/gateway/rpc-handlers/index.ts +++ b/gateway/src/gateway/rpc-handlers/index.ts @@ -58,6 +58,16 @@ import { handleTransferComplete, handleTransferDone, } from "./transfer"; +import { + handleSurfaceOpen, + handleSurfaceClose, + handleSurfaceUpdate, + handleSurfaceFocus, + handleSurfaceList, + handleSurfaceEval, + handleSurfaceEvalResult, +} from "./surface"; +import { handleFsAuthorize } from "./fs"; export function buildRpcHandlers(): Partial<{ [M in RpcMethod]: Handler }> { return { @@ -111,5 +121,13 @@ 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, + "surface.eval": handleSurfaceEval, + "surface.eval.result": handleSurfaceEvalResult, + "fs.authorize": handleFsAuthorize, }; } diff --git a/gateway/src/gateway/rpc-handlers/surface.ts b/gateway/src/gateway/rpc-handlers/surface.ts new file mode 100644 index 0000000..f234a12 --- /dev/null +++ b/gateway/src/gateway/rpc-handlers/surface.ts @@ -0,0 +1,295 @@ +import { env } from "cloudflare:workers"; +import { RpcError } from "../../shared/utils"; +import { snapshot, type Proxied } from "../../shared/persisted-object"; +import type { Handler } from "../../protocol/methods"; +import { DEFER_RESPONSE } from "../../protocol/methods"; +import type { Surface } from "../../protocol/surface"; +import type { + SurfaceOpenedPayload, + SurfaceClosedPayload, + SurfaceUpdatedPayload, + SurfaceEvalRequestPayload, +} 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}`); + } + + // Release browser profile lock if held + const profileId = surface.profileId; + if (profileId) { + gw.releaseProfileLock(profileId, 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, + profileId, + }, 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, + }; +}; + +/** + * Execute JavaScript in a webview surface. + * This is a deferred handler — we send the eval request to the target node + * and wait for `surface.eval.result` to come back before responding. + */ +export const handleSurfaceEval: Handler<"surface.eval"> = ({ gw, ws, frame, params }) => { + if (!params?.surfaceId || !params?.script) { + throw new RpcError(400, "surfaceId and script are required"); + } + + const surface = gw.surfaces[params.surfaceId]; + if (!surface) { + throw new RpcError(404, `Surface not found: ${params.surfaceId}`); + } + + // Only webview surfaces support eval + if (surface.kind !== "webview") { + throw new RpcError(400, `Surface kind "${surface.kind}" does not support eval`); + } + + const targetClientId = surface.targetClientId; + + // Must be targeting a node (not a web client iframe) + if (!gw.nodes.has(targetClientId)) { + throw new RpcError(400, "Surface target is not a display node — eval requires native webview"); + } + + const targetWs = gw.nodes.get(targetClientId); + if (!targetWs || targetWs.readyState !== WebSocket.OPEN) { + throw new RpcError(503, `Target node "${targetClientId}" is not connected`); + } + + const evalId = params.evalId ?? crypto.randomUUID(); + + // Store the pending eval so we can send the deferred response when the result arrives + gw.pendingEvals.set(evalId, { ws, frameId: frame.id }); + + // Send the eval request to the target node as an event + const evalPayload: SurfaceEvalRequestPayload = { + evalId, + surfaceId: params.surfaceId, + script: params.script, + }; + targetWs.send(JSON.stringify({ type: "evt", event: "surface.eval", payload: evalPayload })); + + console.log(`[Gateway] Surface eval dispatched: ${evalId} -> surface ${params.surfaceId}`); + + // Defer the response — it will be sent when surface.eval.result arrives + return DEFER_RESPONSE; +}; + +/** + * Receive the result of a surface eval from a node. + * Routes back to Session DO (agent tool path) and/or resolves WS deferred response. + */ +export const handleSurfaceEvalResult: Handler<"surface.eval.result"> = async ({ gw, params }) => { + if (!params?.evalId) { + throw new RpcError(400, "evalId is required"); + } + + console.log( + `[Gateway] surface.eval.result received: evalId=${params.evalId} ok=${params.ok} hasResult=${params.result !== undefined} result=${JSON.stringify(params.result)?.slice(0, 200)} error=${params.error}`, + ); + + // ── Agent tool path: route result back to Session DO via toolResult() ── + const route = gw.pendingEvalRoutes[params.evalId]; + if (route && typeof route === "object" && route.sessionKey && route.callId) { + const toolResult = params.ok + ? { callId: route.callId, result: { evalId: params.evalId, surfaceId: params.surfaceId, value: params.result } } + : { callId: route.callId, error: params.error || "Eval failed" }; + console.log( + `[Gateway] Routing eval result to session ${route.sessionKey} callId=${route.callId} toolResult=${JSON.stringify(toolResult)?.slice(0, 300)}`, + ); + try { + const sessionStub = env.SESSION.getByName(route.sessionKey); + await sessionStub.toolResult(toolResult); + } catch (e) { + console.error(`[Gateway] Failed to route eval result to session ${route.sessionKey}:`, e); + } + delete gw.pendingEvalRoutes[params.evalId]; + } else { + console.log( + `[Gateway] No agent route found for evalId=${params.evalId} (pendingEvalRoutes keys: ${Object.keys(gw.pendingEvalRoutes).join(", ")})`, + ); + } + + // ── WS deferred path: direct WS caller → pendingEvals ── + const pending = gw.pendingEvals.get(params.evalId); + if (pending) { + gw.pendingEvals.delete(params.evalId); + + // Send the deferred response to the original WS caller + const response = { + type: "res" as const, + id: pending.frameId, + ok: params.ok, + payload: { + evalId: params.evalId, + surfaceId: params.surfaceId, + ok: params.ok, + result: params.result, + error: params.error, + }, + }; + + if (pending.ws.readyState === WebSocket.OPEN) { + pending.ws.send(JSON.stringify(response)); + } + } + + return { ok: true as const }; +}; diff --git a/gateway/src/index.ts b/gateway/src/index.ts index a2c20e9..b1c0e99 100644 --- a/gateway/src/index.ts +++ b/gateway/src/index.ts @@ -97,9 +97,84 @@ export default { return stub.fetch(request); } - // Serve media files from R2 - // /media/{uuid}.{ext} - // TODO: either remove or auth this + // ── Authenticated R2 access: /fs/{r2-key} ── + // Clients obtain a short-lived token via the `fs.authorize` WS RPC, + // then use it as a Bearer header here for direct R2 read/write. + const fsMatch = url.pathname.match(/^\/fs\/(.+)$/); + if (fsMatch && (request.method === "GET" || request.method === "PUT")) { + const r2Key = decodeURIComponent(fsMatch[1]); + + // Extract Bearer token + const authHeader = request.headers.get("Authorization"); + const token = authHeader?.startsWith("Bearer ") + ? authHeader.slice(7) + : null; + if (!token) { + return new Response("Unauthorized", { status: 401 }); + } + + // Path traversal guard + if (r2Key.includes("..")) { + return new Response("Bad Request", { status: 400 }); + } + + // Verify token against the Gateway DO + const stub = env.GATEWAY.get(env.GATEWAY.idFromName("singleton")); + const mode = request.method === "GET" ? "read" : "write"; + const valid = await stub.verifyFsToken(token, r2Key, mode as "read" | "write"); + if (!valid) { + return new Response("Forbidden", { status: 403 }); + } + + if (request.method === "GET") { + const object = await env.STORAGE.get(r2Key); + if (!object) { + return new Response("Not Found", { status: 404 }); + } + const headers = new Headers(); + headers.set( + "Content-Type", + object.httpMetadata?.contentType || "application/octet-stream", + ); + headers.set("Cache-Control", "private, no-cache"); + return new Response(object.body, { headers }); + } + + // PUT — write to R2 + if (!request.body) { + return new Response("Body required", { status: 400 }); + } + + // Enforce 50 MB limit + const contentLength = request.headers.get("Content-Length"); + if (contentLength && parseInt(contentLength, 10) > 50 * 1024 * 1024) { + return new Response("Payload Too Large", { status: 413 }); + } + + // Extract custom metadata from headers (optional) + const customMetadata: Record = {}; + const metaHeader = request.headers.get("X-R2-Meta"); + if (metaHeader) { + try { + const parsed = JSON.parse(metaHeader); + for (const [k, v] of Object.entries(parsed)) { + if (typeof v === "string") customMetadata[k] = v; + } + } catch { /* ignore malformed metadata */ } + } + + await env.STORAGE.put(r2Key, request.body, { + httpMetadata: { + contentType: + request.headers.get("Content-Type") || "application/octet-stream", + }, + customMetadata, + }); + + return new Response("OK", { status: 200 }); + } + + // Legacy media endpoint (unauthenticated, to be migrated to /fs/) const mediaMatch = url.pathname.match( /^\/media\/([a-f0-9-]+\.[a-z0-9]+)$/i, ); @@ -114,7 +189,6 @@ export default { // Check if expired const expiresAt = object.customMetadata?.expiresAt; if (expiresAt && parseInt(expiresAt, 10) < Date.now()) { - // Clean up expired file await env.STORAGE.delete(key); return new Response("Expired", { status: 410 }); } @@ -125,7 +199,6 @@ export default { object.httpMetadata?.contentType || "application/octet-stream", ); headers.set("Cache-Control", "private, max-age=3600"); - // Allow cross-origin for LLM APIs headers.set("Access-Control-Allow-Origin", "*"); return new Response(object.body, { headers }); diff --git a/gateway/src/protocol/fs.ts b/gateway/src/protocol/fs.ts new file mode 100644 index 0000000..b7196d1 --- /dev/null +++ b/gateway/src/protocol/fs.ts @@ -0,0 +1,33 @@ +/** + * Filesystem (R2) access protocol. + * + * Clients request short-lived tokens via the `fs.authorize` RPC over WebSocket. + * The token is then used as a Bearer header on HTTP requests to `/fs/{r2-key}`. + * The worker fetch handler validates the token against the Gateway DO before + * proxying the R2 read/write. + */ + +export type FsMode = "read" | "write"; + +export type FsAuthorizeParams = { + /** R2 key prefix this token grants access to (e.g. "browser-profiles/github.com/") */ + pathPrefix: string; + /** read = GET, write = PUT */ + mode: FsMode; +}; + +export type FsAuthorizeResult = { + /** Opaque token to use as Bearer header */ + token: string; + /** When this token expires (unix ms) */ + expiresAt: number; + /** The /fs/ URL path prefix to use */ + pathPrefix: string; +}; + +/** Internal token record stored in Gateway DO memory. */ +export type FsToken = { + pathPrefix: string; + mode: FsMode; + expiresAt: number; +}; diff --git a/gateway/src/protocol/methods.ts b/gateway/src/protocol/methods.ts index 024fe5c..6f9b06c 100644 --- a/gateway/src/protocol/methods.ts +++ b/gateway/src/protocol/methods.ts @@ -35,6 +35,21 @@ import type { TransferCompleteParams, TransferDoneParams, } from "./transfer"; +import type { + SurfaceOpenParams, + SurfaceOpenResult, + SurfaceCloseParams, + SurfaceCloseResult, + SurfaceUpdateParams, + SurfaceUpdateResult, + SurfaceFocusParams, + SurfaceFocusResult, + SurfaceListParams, + SurfaceListResult, + SurfaceEvalParams, + SurfaceEvalResult, +} from "./surface"; +import type { FsAuthorizeParams, FsAuthorizeResult } from "./fs"; import type { CronJob, CronJobCreate, @@ -425,6 +440,41 @@ 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; + }; + + "surface.eval": { + params: SurfaceEvalParams; + result: SurfaceEvalResult; + }; + + "surface.eval.result": { + params: SurfaceEvalResult; + result: { ok: true }; + }; + "tool.request": { params: ToolRequestParams; result: { @@ -456,10 +506,15 @@ export type RpcMethods = { params: TransferDoneParams; result: { ok: true }; }; + + "fs.authorize": { + params: FsAuthorizeParams; + result: FsAuthorizeResult; + }; }; export type RpcMethod = keyof RpcMethods; -export type DeferrableMethod = "tool.invoke" | "logs.get"; +export type DeferrableMethod = "tool.invoke" | "logs.get" | "surface.eval"; export type ParamsOf = RpcMethods[M]["params"]; export type ResultOf = RpcMethods[M]["result"]; export type HandlerResult = diff --git a/gateway/src/protocol/surface.ts b/gateway/src/protocol/surface.ts new file mode 100644 index 0000000..aa35a2a --- /dev/null +++ b/gateway/src/protocol/surface.ts @@ -0,0 +1,164 @@ +/** + * 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; + // Browser profile persistence (webview surfaces) + profileId?: string; // derived from URL origin (e.g. "github.com") + profileVersion?: number; // monotonic, incremented on each R2 upload + profileLock?: { // set when a node has the profile open + nodeId: string; + surfaceId: string; + }; +}; + +// ── 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; +}; + +// ── Eval (JavaScript execution in webview surfaces) ── + +export type SurfaceEvalParams = { + surfaceId: string; + /** JavaScript code to execute in the webview context. */ + script: string; + /** Unique eval ID for correlating the async result. Auto-generated if omitted. */ + evalId?: string; +}; + +export type SurfaceEvalResult = { + evalId: string; + surfaceId: string; + /** True if the script executed without throwing. */ + ok: boolean; + /** JSON-serializable return value from the script (if ok). */ + result?: unknown; + /** Error message (if !ok). */ + error?: string; +}; + +// ── Event payloads ── + +export type SurfaceOpenedPayload = { + surface: Surface; +}; + +export type SurfaceClosedPayload = { + surfaceId: string; + targetClientId: string; + profileId?: string; +}; + +export type SurfaceUpdatedPayload = { + surface: Surface; +}; + +/** Sent by gateway to the target node to request JS execution. */ +export type SurfaceEvalRequestPayload = { + evalId: string; + surfaceId: string; + script: string; +}; + +/** Sent by the node back to gateway with the eval result. */ +export type SurfaceEvalResultPayload = { + evalId: string; + surfaceId: string; + ok: boolean; + result?: unknown; + error?: string; +}; 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 937530f..b28decc 100644 --- a/gateway/src/session/do.ts +++ b/gateway/src/session/do.ts @@ -64,6 +64,7 @@ import { parseToolApprovalDecision, type ToolApprovalEvaluation, } from "./tool-approval"; +import { Agent, Connection, ConnectionContext } from "agents"; type PendingToolCall = { id: string; @@ -295,6 +296,7 @@ export class Session extends DurableObject { return crypto.randomUUID(); } + /** * Extract agentId from session key. * Session key format: agent:{agentId}:{...} @@ -1243,10 +1245,12 @@ export class Session extends DurableObject { // If this turn resumed from tool calls, inject queued user messages now so they // are included before the next LLM continuation, without waiting for run end. - let continuationOverrides: { - thinkLevel?: string; - model?: { provider: string; id: string }; - } | undefined; + let continuationOverrides: + | { + thinkLevel?: string; + model?: { provider: string; id: string }; + } + | undefined; if (hadPendingToolCalls && this.messageQueue.length > 0) { const queuedMessages = [...this.messageQueue]; this.messageQueue = []; @@ -2101,11 +2105,20 @@ export class Session extends DurableObject { bucket: this.env.STORAGE, agentId, gateway, + callId: toolCall.id, + sessionKey: this.meta.sessionKey, }, toolCall.name, toolCall.args, ); + // Deferred tools (e.g. eval) will have their result routed back + // asynchronously via toolResult(). Don't set result/error here. + if (result.deferred) { + console.log(`[Session] Native tool ${toolCall.name} deferred (callId=${toolCall.id})`); + return; + } + if (result.ok) { toolCall.result = result.result; } else { diff --git a/gateway/ui/index.html b/gateway/ui/index.html index 6ca32a7..81ca668 100644 --- a/gateway/ui/index.html +++ b/gateway/ui/index.html @@ -7,7 +7,7 @@ - +
diff --git a/gateway/ui/src/react/App.tsx b/gateway/ui/src/react/App.tsx index bb9d4c8..f785a8b 100644 --- a/gateway/ui/src/react/App.tsx +++ b/gateway/ui/src/react/App.tsx @@ -1,78 +1,32 @@ -import { lazy, Suspense, 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, type Tab } from "../ui/types"; +import { OsShell } from "./components/OsShell"; import { useReactUiStore } from "./state/store"; - -const ChatView = lazy(() => - import("./views/ChatView").then((module) => ({ default: module.ChatView })), -); -const OverviewView = lazy(() => - import("./views/OverviewView").then((module) => ({ - default: module.OverviewView, - })), -); -const SessionsView = lazy(() => - import("./views/SessionsView").then((module) => ({ - default: module.SessionsView, - })), -); -const ChannelsView = lazy(() => - import("./views/ChannelsView").then((module) => ({ - default: module.ChannelsView, - })), -); -const NodesView = lazy(() => - import("./views/NodesView").then((module) => ({ default: module.NodesView })), -); -const WorkspaceView = lazy(() => - import("./views/WorkspaceView").then((module) => ({ - default: module.WorkspaceView, - })), -); -const CronView = lazy(() => - import("./views/CronView").then((module) => ({ default: module.CronView })), -); -const LogsView = lazy(() => - import("./views/LogsView").then((module) => ({ default: module.LogsView })), -); -const PairingView = lazy(() => - import("./views/PairingView").then((module) => ({ - default: module.PairingView, - })), -); -const ConfigView = lazy(() => - import("./views/ConfigView").then((module) => ({ - default: module.ConfigView, - })), -); -const DebugView = lazy(() => - import("./views/DebugView").then((module) => ({ default: module.DebugView })), -); +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 connectionState = useReactUiStore((s) => s.connectionState); + const updateSettings = useReactUiStore((s) => s.updateSettings); + const settings = useReactUiStore((s) => s.settings); + const disconnect = useReactUiStore((s) => s.disconnect); 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(); @@ -82,12 +36,32 @@ export function App() { }; }, [syncTabFromLocation]); - const showConnectScreen = useReactUiStore((s) => s.showConnectScreen); + useEffect(() => { + preloadTabView(tab); + }, [tab]); + if (showConnectScreen) { return ; } - return ; + return ( + + updateSettings({ + theme: settings.theme === "dark" ? "light" : "dark", + }) + } + onChangeWallpaper={(wp) => updateSettings({ wallpaper: wp })} + onChangeShellStyle={(shellStyle) => updateSettings({ shellStyle })} + onDisconnect={disconnect} + /> + ); } function ConnectScreen() { @@ -113,7 +87,7 @@ function ConnectScreen() {
- + GSV

GSV

Gateway control UI

@@ -165,162 +139,3 @@ function ConnectScreen() {
); } - -function MainShell() { - 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 ( -
- - ))} -
- ))} - - -
-
- - {connectionState} - -
-
- - -
-
-
- -

{TAB_LABELS[tab]}

-
-
- - -
-
- -
- -
-
- - ); -} - -function ReactTabView({ tab }: { tab: Tab }) { - return ( - }> - {tab === "chat" ? : null} - {tab === "overview" ? : null} - {tab === "sessions" ? : null} - {tab === "channels" ? : null} - {tab === "nodes" ? : null} - {tab === "workspace" ? : null} - {tab === "cron" ? : null} - {tab === "logs" ? : null} - {tab === "pairing" ? : null} - {tab === "config" ? : null} - {tab === "debug" ? : null} - {!TAB_LABELS[tab] ? ( -
- -
-

Unknown tab: {tab}

-
-
-
- ) : null} -
- ); -} - -function TabLoadingFallback() { - return ( -
- -
-
- - Loading view... -
-
-
-
- ); -} diff --git a/gateway/ui/src/react/components/OsShell.tsx b/gateway/ui/src/react/components/OsShell.tsx new file mode 100644 index 0000000..b661ff3 --- /dev/null +++ b/gateway/ui/src/react/components/OsShell.tsx @@ -0,0 +1,1778 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type CSSProperties, + type MouseEvent as ReactMouseEvent, + type PointerEvent as ReactPointerEvent, +} from "react"; +import { TAB_ICONS, TAB_LABELS, OS_DOCK_TABS, TAB_GROUPS, type Tab, type Surface } from "../../ui/types"; +import type { ShellStyle, Wallpaper } from "../../ui/storage"; +import { preloadTabView, TabView } from "../tabViews"; +import { WallpaperBg } from "./Wallpaper"; +import { useReactUiStore } from "../state/store"; + +const WINDOW_MIN_WIDTH = 420; +const WINDOW_MIN_HEIGHT = 280; +const WINDOW_MARGIN = 12; +const SNAP_THRESHOLD = 28; +const CLOCK_REFRESH_MS = 15_000; + +/** + * Normalize a URL to its embeddable form for known services. + * Returns the embed URL if a transformation applies, or the original URL otherwise. + */ +function toEmbedUrl(raw: string): string { + try { + const u = new URL(raw); + const host = u.hostname.replace(/^www\./, ""); + + // YouTube: watch?v=ID → /embed/ID, youtu.be/ID → /embed/ID, shorts/ID → /embed/ID + if (host === "youtube.com" || host === "m.youtube.com") { + const videoId = u.searchParams.get("v"); + if (videoId) return `https://www.youtube.com/embed/${videoId}?autoplay=1`; + const shortsMatch = u.pathname.match(/^\/shorts\/([^/?]+)/); + if (shortsMatch) return `https://www.youtube.com/embed/${shortsMatch[1]}?autoplay=1`; + if (u.pathname.startsWith("/embed/")) { + u.searchParams.set("autoplay", "1"); + return u.toString(); + } + } + if (host === "youtu.be") { + const videoId = u.pathname.slice(1).split("/")[0]; + if (videoId) return `https://www.youtube.com/embed/${videoId}?autoplay=1`; + } + + // Vimeo: vimeo.com/ID → player.vimeo.com/video/ID + if (host === "vimeo.com") { + const videoId = u.pathname.match(/^\/(\d+)/)?.[1]; + if (videoId) return `https://player.vimeo.com/video/${videoId}?autoplay=1`; + } + if (host === "player.vimeo.com") { + u.searchParams.set("autoplay", "1"); + return u.toString(); + } + + // Spotify: open.spotify.com/track/ID → open.spotify.com/embed/track/ID (etc.) + if (host === "open.spotify.com" && !u.pathname.startsWith("/embed/")) { + return `https://open.spotify.com/embed${u.pathname}`; + } + + // Google Maps: /maps/... → /maps/embed/... + if (host === "google.com" && u.pathname.startsWith("/maps") && !u.pathname.includes("/embed")) { + return `https://www.google.com/maps/embed/v1/place?key=&q=${encodeURIComponent(raw)}`; + } + + // Figma: figma.com/file/... or figma.com/design/... → embed + if (host === "figma.com" && (u.pathname.startsWith("/file/") || u.pathname.startsWith("/design/"))) { + return `https://www.figma.com/embed?embed_host=gsv&url=${encodeURIComponent(raw)}`; + } + + // Loom: loom.com/share/ID → loom.com/embed/ID + if (host === "loom.com" || host === "www.loom.com") { + const shareMatch = u.pathname.match(/^\/share\/([^/?]+)/); + if (shareMatch) return `https://www.loom.com/embed/${shareMatch[1]}?autoplay=1`; + } + + return raw; + } catch { + return raw; + } +} + +const CLOCK_FORMATTER = new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", +}); + +type WindowRect = { + x: number; + y: number; + width: number; + height: number; +}; + +type OsWindow = WindowRect & { + id: number; + tab: Tab; + z: number; + minimized: boolean; + maximized: boolean; + snapped: "left" | "right" | null; + restoreRect?: WindowRect; + surfaceId?: string; + /** URL for webview/media surfaces (renders as iframe instead of TabView). */ + url?: string; + /** Custom title label for webview/media windows. */ + surfaceLabel?: string; +}; + +type ResizeEdge = "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw"; +type SnapZone = "left" | "right" | "top"; + +type InteractionState = + | { + type: "drag"; + windowId: number; + startClientX: number; + startClientY: number; + startRect: WindowRect; + } + | { + type: "resize"; + windowId: number; + edge: ResizeEdge; + startClientX: number; + startClientY: number; + startRect: WindowRect; + }; + +type OpenWindowOptions = { + syncTab?: boolean; + newWindow?: boolean; +}; + +type SnapPreviewState = { + zone: SnapZone; + rect: WindowRect; +}; + +type CommandAction = { + id: string; + label: string; + hint?: string; + keywords?: string[]; + run: () => void; +}; + +type OsShellProps = { + tab: Tab; + onSwitchTab: (tab: Tab) => void; + connectionState: "connected" | "connecting" | "disconnected"; + theme: "dark" | "light" | "system"; + wallpaper: Wallpaper; + shellStyle: ShellStyle; + onToggleTheme: () => void; + onChangeWallpaper: (wallpaper: Wallpaper) => void; + onChangeShellStyle: (shellStyle: ShellStyle) => void; + onDisconnect: () => void; +}; + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function isTypingTarget(target: EventTarget | null): boolean { + const element = target as HTMLElement | null; + if (!element) { + return false; + } + const tagName = element.tagName; + return ( + element.isContentEditable || + tagName === "INPUT" || + tagName === "TEXTAREA" || + tagName === "SELECT" + ); +} + +function shouldOpenNewWindow(event: ReactMouseEvent): boolean { + return event.altKey || event.shiftKey || event.metaKey || event.ctrlKey; +} + +function formatClock(date: Date): string { + return CLOCK_FORMATTER.format(date); +} + +function getDesktopBounds(node: HTMLDivElement | null): WindowRect { + if (node) { + const rect = node.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + return { + x: 0, + y: 0, + width: Math.round(rect.width), + height: Math.round(rect.height), + }; + } + } + + if (typeof window !== "undefined") { + return { + x: 0, + y: 0, + width: Math.max(720, Math.round(window.innerWidth - 320)), + height: Math.max(420, Math.round(window.innerHeight - 140)), + }; + } + + return { x: 0, y: 0, width: 960, height: 640 }; +} + +function getDesktopClientBounds(node: HTMLDivElement | null): { + left: number; + top: number; + width: number; + height: number; +} { + if (node) { + const rect = node.getBoundingClientRect(); + return { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }; + } + + if (typeof window !== "undefined") { + return { + left: 0, + top: 0, + width: window.innerWidth, + height: window.innerHeight, + }; + } + + return { left: 0, top: 0, width: 960, height: 640 }; +} + +function clampRectToBounds(rect: WindowRect, bounds: WindowRect): WindowRect { + const maxWidth = Math.max(WINDOW_MIN_WIDTH, bounds.width - WINDOW_MARGIN * 2); + const maxHeight = Math.max(WINDOW_MIN_HEIGHT, bounds.height - WINDOW_MARGIN * 2); + const width = clamp(rect.width, WINDOW_MIN_WIDTH, maxWidth); + const height = clamp(rect.height, WINDOW_MIN_HEIGHT, maxHeight); + const maxX = Math.max(WINDOW_MARGIN, bounds.width - width - WINDOW_MARGIN); + const maxY = Math.max(WINDOW_MARGIN, bounds.height - height - WINDOW_MARGIN); + return { + x: clamp(rect.x, WINDOW_MARGIN, maxX), + y: clamp(rect.y, WINDOW_MARGIN, maxY), + width, + height, + }; +} + +function createWindow( + tab: Tab, + id: number, + z: number, + index: number, + bounds: WindowRect, +): OsWindow { + const baseRect: WindowRect = { + x: 36 + (index % 7) * 24, + y: 34 + (index % 7) * 18, + width: Math.round(bounds.width * 0.68), + height: Math.round(bounds.height * 0.74), + }; + const rect = clampRectToBounds(baseRect, bounds); + return { + id, + tab, + z, + minimized: false, + maximized: false, + snapped: null, + ...rect, + }; +} + +function resizeRect( + startRect: WindowRect, + edge: ResizeEdge, + deltaX: number, + deltaY: number, + bounds: WindowRect, +): WindowRect { + let nextX = startRect.x; + let nextY = startRect.y; + let nextWidth = startRect.width; + let nextHeight = startRect.height; + + if (edge.includes("e")) { + const maxWidth = bounds.width - startRect.x - WINDOW_MARGIN; + nextWidth = clamp(startRect.width + deltaX, WINDOW_MIN_WIDTH, maxWidth); + } + + if (edge.includes("s")) { + const maxHeight = bounds.height - startRect.y - WINDOW_MARGIN; + nextHeight = clamp(startRect.height + deltaY, WINDOW_MIN_HEIGHT, maxHeight); + } + + if (edge.includes("w")) { + const maxX = startRect.x + startRect.width - WINDOW_MIN_WIDTH; + nextX = clamp(startRect.x + deltaX, WINDOW_MARGIN, maxX); + nextWidth = startRect.width - (nextX - startRect.x); + } + + if (edge.includes("n")) { + const maxY = startRect.y + startRect.height - WINDOW_MIN_HEIGHT; + nextY = clamp(startRect.y + deltaY, WINDOW_MARGIN, maxY); + nextHeight = startRect.height - (nextY - startRect.y); + } + + return clampRectToBounds( + { + x: nextX, + y: nextY, + width: nextWidth, + height: nextHeight, + }, + bounds, + ); +} + +function detectSnapZone( + clientX: number, + clientY: number, + desktopClientBounds: { left: number; top: number; width: number; height: number }, +): SnapZone | null { + const localX = clientX - desktopClientBounds.left; + const localY = clientY - desktopClientBounds.top; + + if ( + localX < 0 || + localY < 0 || + localX > desktopClientBounds.width || + localY > desktopClientBounds.height + ) { + return null; + } + + if (localY <= SNAP_THRESHOLD) { + return "top"; + } + if (localX <= SNAP_THRESHOLD) { + return "left"; + } + if (localX >= desktopClientBounds.width - SNAP_THRESHOLD) { + return "right"; + } + return null; +} + +function getSnapRect(zone: SnapZone, bounds: WindowRect): WindowRect { + if (zone === "top") { + return { + x: 0, + y: 0, + width: bounds.width, + height: bounds.height, + }; + } + + const halfWidth = Math.max(WINDOW_MIN_WIDTH, Math.floor(bounds.width / 2)); + if (zone === "left") { + return { + x: 0, + y: 0, + width: halfWidth, + height: bounds.height, + }; + } + + return { + x: bounds.width - halfWidth, + y: 0, + width: halfWidth, + height: bounds.height, + }; +} + +const RESIZE_HANDLES: { edge: ResizeEdge; className: string }[] = [ + { edge: "n", className: "os-resize-handle n" }, + { edge: "s", className: "os-resize-handle s" }, + { edge: "e", className: "os-resize-handle e" }, + { edge: "w", className: "os-resize-handle w" }, + { edge: "ne", className: "os-resize-handle ne" }, + { edge: "nw", className: "os-resize-handle nw" }, + { edge: "se", className: "os-resize-handle se" }, + { edge: "sw", className: "os-resize-handle sw" }, +]; + +export function OsShell({ + tab, + onSwitchTab, + connectionState, + theme, + wallpaper, + shellStyle: shellVariant, + onToggleTheme, + onChangeWallpaper, + onChangeShellStyle, + onDisconnect, +}: OsShellProps) { + // ── Surface protocol integration ── + const storeSurfaces = useReactUiStore((s) => s.surfaces); + const storeClientId = useReactUiStore((s) => s.clientId); + const storeSurfaceOpen = useReactUiStore((s) => s.surfaceOpen); + const storeSurfaceClose = useReactUiStore((s) => s.surfaceClose); + const storeSurfaceUpdate = useReactUiStore((s) => s.surfaceUpdate); + const storeSurfaceFocus = useReactUiStore((s) => s.surfaceFocus); + + const desktopRef = useRef(null); + const commandInputRef = useRef(null); + const interactionRef = useRef(null); + const windowsRef = useRef([]); + const snapPreviewRef = useRef(null); + const focusedWindowIdRef = useRef(1); + const nextWindowIdRef = useRef(2); + const nextZRef = useRef(2); + // Track which surfaceIds this client owns (locally opened), to avoid echo loops. + const ownedSurfaceIdsRef = useRef>(new Set()); + // Track window IDs that have a surfaceOpen RPC in flight (surfaceId not yet known). + // Maps windowId → contentRef (tab name) so reconciliation can match pending opens. + const pendingSurfaceOpensRef = useRef>(new Map()); + // Stable refs for surface actions (avoid callback dependency churn). + const surfaceOpenRef = useRef(storeSurfaceOpen); + surfaceOpenRef.current = storeSurfaceOpen; + const surfaceCloseRef = useRef(storeSurfaceClose); + surfaceCloseRef.current = storeSurfaceClose; + const surfaceUpdateRef = useRef(storeSurfaceUpdate); + surfaceUpdateRef.current = storeSurfaceUpdate; + const surfaceFocusRef = useRef(storeSurfaceFocus); + surfaceFocusRef.current = storeSurfaceFocus; + + const [windows, setWindows] = useState(() => { + const bounds = getDesktopBounds(null); + // Mark the initial window as pending so reconciliation doesn't duplicate it. + pendingSurfaceOpensRef.current.set(1, tab); + return [createWindow(tab, 1, 1, 0, bounds)]; + }); + const [focusedWindowId, setFocusedWindowId] = useState(1); + const [snapPreview, setSnapPreview] = useState(null); + const [commandOpen, setCommandOpen] = useState(false); + const [commandQuery, setCommandQuery] = useState(""); + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); + const [clockLabel, setClockLabel] = useState(() => formatClock(new Date())); + + const launchTabs = OS_DOCK_TABS; + const allTabs = useMemo(() => TAB_GROUPS.flatMap((group) => group.tabs), []); + /** Windows backed by a gateway surface URL (webview/media) — not tied to a Tab. */ + const surfaceWindows = useMemo( + () => windows.filter((windowState) => windowState.url), + [windows], + ); + const openTabs = useMemo( + () => new Set(windows.filter((w) => !w.url).map((w) => w.tab)), + [windows], + ); + // Do NOT sort by z-index here — sorting reorders the array on every focus + // change, causing React to physically move DOM nodes (reloads iframes, + // resets scroll). CSS z-index (set via inline style) handles stacking. + const visibleWindows = useMemo( + () => windows.filter((windowState) => !windowState.minimized), + [windows], + ); + const focusedWindow = useMemo( + () => + focusedWindowId === null + ? null + : windows.find((windowState) => windowState.id === focusedWindowId) ?? null, + [focusedWindowId, windows], + ); + const focusedIsUrl = focusedWindow?.url != null; + const focusedTab = focusedIsUrl ? null : (focusedWindow?.tab ?? null); + const focusedTabLabel = focusedIsUrl + ? (focusedWindow?.surfaceLabel ?? "Webview") + : focusedTab + ? TAB_LABELS[focusedTab] + : "No focus"; + const totalWindowCount = windows.length; + const visibleWindowCount = visibleWindows.length; + const shellInlineStyle = useMemo( + () => + ({ + "--os-focus-accent": "var(--text-primary)", + }) as CSSProperties, + [], + ); + const connectionStateLabel = useMemo(() => { + if (connectionState === "connected") { + return "online"; + } + if (connectionState === "connecting") { + return "linking"; + } + return "offline"; + }, [connectionState]); + const windowCountByTab = useMemo(() => { + const counts: Partial> = {}; + for (const windowState of windows) { + if (!windowState.url) { + counts[windowState.tab] = (counts[windowState.tab] ?? 0) + 1; + } + } + return counts; + }, [windows]); + + useEffect(() => { + windowsRef.current = windows; + }, [windows]); + + useEffect(() => { + focusedWindowIdRef.current = focusedWindowId; + }, [focusedWindowId]); + + useEffect(() => { + const updateClock = () => { + setClockLabel(formatClock(new Date())); + }; + updateClock(); + const interval = window.setInterval(updateClock, CLOCK_REFRESH_MS); + return () => { + window.clearInterval(interval); + }; + }, []); + + // Register the initial window (id=1) with the gateway surface registry. + // This runs once on mount. The pending marker was set synchronously in useState. + useEffect(() => { + const initialWindow = windowsRef.current.find((w) => w.id === 1); + if (!initialWindow) return; + void surfaceOpenRef.current({ + kind: "app", + contentRef: initialWindow.tab, + label: TAB_LABELS[initialWindow.tab], + rect: { x: initialWindow.x, y: initialWindow.y, width: initialWindow.width, height: initialWindow.height }, + }).then((surface) => { + pendingSurfaceOpensRef.current.delete(1); + if (surface) { + ownedSurfaceIdsRef.current.add(surface.surfaceId); + setWindows((prev) => + prev.map((w) => + w.id === 1 ? { ...w, surfaceId: surface.surfaceId } : w, + ), + ); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const connectionBadgeVariant = useMemo(() => { + if (connectionState === "connected") { + return "primary"; + } + if (connectionState === "connecting") { + return "outline"; + } + return "destructive"; + }, [connectionState]); + + /** Fire-and-forget: sync a window's state to the gateway surface registry. */ + const syncSurfaceState = useCallback( + (win: OsWindow) => { + if (!win.surfaceId) return; + void surfaceUpdateRef.current({ + surfaceId: win.surfaceId, + state: win.minimized ? "minimized" : "open", + rect: { x: win.x, y: win.y, width: win.width, height: win.height }, + zIndex: win.z, + }); + }, + [], + ); + + const focusWindow = useCallback( + (windowId: number, syncTab = true) => { + const target = windowsRef.current.find((windowState) => windowState.id === windowId); + if (!target) { + return; + } + const nextZ = nextZRef.current++; + setWindows((previous) => + previous.map((windowState) => + windowState.id === windowId + ? { + ...windowState, + minimized: false, + z: nextZ, + } + : windowState, + ), + ); + setFocusedWindowId(windowId); + if (syncTab) { + onSwitchTab(target.tab); + } + // Surface sync: focus + if (target.surfaceId) { + void surfaceFocusRef.current(target.surfaceId); + } + }, + [onSwitchTab], + ); + + const openWindow = useCallback( + (windowTab: Tab, options: OpenWindowOptions = {}) => { + const { newWindow = false, syncTab = true } = options; + preloadTabView(windowTab); + + const existingTopWindow = windowsRef.current + .filter((windowState) => windowState.tab === windowTab) + .sort((left, right) => right.z - left.z)[0]; + + if (existingTopWindow && !newWindow) { + focusWindow(existingTopWindow.id, syncTab); + return existingTopWindow.id; + } + + const bounds = getDesktopBounds(desktopRef.current); + const windowId = nextWindowIdRef.current++; + const nextZ = nextZRef.current++; + const windowState = createWindow( + windowTab, + windowId, + nextZ, + windowsRef.current.length, + bounds, + ); + + setWindows((previous) => [...previous, windowState]); + setFocusedWindowId(windowId); + if (syncTab) { + onSwitchTab(windowTab); + } + + // Surface sync: register this window with the gateway. + // Mark as pending SYNCHRONOUSLY so reconciliation skips duplicates + // even if the surfaceOpen RPC resolves before the .then() patches the window. + pendingSurfaceOpensRef.current.set(windowId, windowTab); + void surfaceOpenRef.current({ + kind: "app", + contentRef: windowTab, + label: TAB_LABELS[windowTab], + rect: { x: windowState.x, y: windowState.y, width: windowState.width, height: windowState.height }, + }).then((surface) => { + pendingSurfaceOpensRef.current.delete(windowId); + if (surface) { + ownedSurfaceIdsRef.current.add(surface.surfaceId); + // Patch the window with its surfaceId + setWindows((prev) => + prev.map((w) => + w.id === windowId ? { ...w, surfaceId: surface.surfaceId } : w, + ), + ); + } + }); + + return windowId; + }, + [focusWindow, onSwitchTab], + ); + + const closeWindow = useCallback( + (windowId: number) => { + const currentWindows = windowsRef.current; + const target = currentWindows.find((windowState) => windowState.id === windowId); + if (!target) { + return; + } + + // Surface sync: close + if (target.surfaceId) { + ownedSurfaceIdsRef.current.delete(target.surfaceId); + void surfaceCloseRef.current(target.surfaceId); + } + + const remaining = currentWindows.filter((windowState) => windowState.id !== windowId); + if (!remaining.length) { + setWindows([]); + setFocusedWindowId(null); + return; + } + + if (focusedWindowIdRef.current !== windowId) { + setWindows(remaining); + return; + } + + const nextFocusTarget = + remaining + .filter((windowState) => !windowState.minimized) + .sort((left, right) => right.z - left.z)[0] ?? + [...remaining].sort((left, right) => right.z - left.z)[0]; + const nextZ = nextZRef.current++; + + setWindows( + remaining.map((windowState) => + windowState.id === nextFocusTarget.id + ? { + ...windowState, + minimized: false, + z: nextZ, + } + : windowState, + ), + ); + setFocusedWindowId(nextFocusTarget.id); + onSwitchTab(nextFocusTarget.tab); + }, + [onSwitchTab], + ); + + const minimizeWindow = useCallback( + (windowId: number) => { + const currentWindows = windowsRef.current; + const target = currentWindows.find((windowState) => windowState.id === windowId); + if (!target) { + return; + } + + setWindows((previous) => + previous.map((windowState) => + windowState.id === windowId + ? { + ...windowState, + minimized: true, + } + : windowState, + ), + ); + + // Surface sync: minimized + if (target.surfaceId) { + void surfaceUpdateRef.current({ + surfaceId: target.surfaceId, + state: "minimized", + }); + } + + if (focusedWindowIdRef.current !== windowId) { + return; + } + + const nextVisible = currentWindows + .filter((windowState) => windowState.id !== windowId && !windowState.minimized) + .sort((left, right) => right.z - left.z); + if (!nextVisible.length) { + setFocusedWindowId(null); + return; + } + focusWindow(nextVisible[0].id); + }, + [focusWindow], + ); + + const restoreWindow = useCallback( + (windowId: number) => { + const target = windowsRef.current.find((windowState) => windowState.id === windowId); + if (!target || !target.restoreRect) { + return; + } + const bounds = getDesktopBounds(desktopRef.current); + const restored = clampRectToBounds(target.restoreRect, bounds); + const nextZ = nextZRef.current++; + + setWindows((previous) => + previous.map((windowState) => + windowState.id === windowId + ? { + ...windowState, + ...restored, + z: nextZ, + maximized: false, + snapped: null, + restoreRect: undefined, + } + : windowState, + ), + ); + setFocusedWindowId(windowId); + onSwitchTab(target.tab); + + // Surface sync: restored rect + if (target.surfaceId) { + void surfaceUpdateRef.current({ + surfaceId: target.surfaceId, + state: "open", + rect: restored, + zIndex: nextZ, + }); + } + }, + [onSwitchTab], + ); + + const toggleWindowMaximized = useCallback( + (windowId: number) => { + const current = windowsRef.current.find((windowState) => windowState.id === windowId); + if (!current) { + return; + } + if (current.maximized) { + restoreWindow(windowId); + return; + } + + const bounds = getDesktopBounds(desktopRef.current); + const nextZ = nextZRef.current++; + + setWindows((previous) => + previous.map((windowState) => { + if (windowState.id !== windowId) { + return windowState; + } + return { + ...windowState, + x: 0, + y: 0, + width: bounds.width, + height: bounds.height, + maximized: true, + snapped: null, + restoreRect: { + x: windowState.x, + y: windowState.y, + width: windowState.width, + height: windowState.height, + }, + z: nextZ, + }; + }), + ); + setFocusedWindowId(windowId); + onSwitchTab(current.tab); + + // Surface sync: maximized rect + if (current.surfaceId) { + void surfaceUpdateRef.current({ + surfaceId: current.surfaceId, + state: "open", + rect: { x: 0, y: 0, width: bounds.width, height: bounds.height }, + zIndex: nextZ, + }); + } + }, + [onSwitchTab, restoreWindow], + ); + + const snapWindow = useCallback( + (windowId: number, zone: SnapZone) => { + const target = windowsRef.current.find((windowState) => windowState.id === windowId); + if (!target) { + return; + } + const bounds = getDesktopBounds(desktopRef.current); + const snappedRect = clampRectToBounds(getSnapRect(zone, bounds), bounds); + const nextZ = nextZRef.current++; + + setWindows((previous) => + previous.map((windowState) => { + if (windowState.id !== windowId) { + return windowState; + } + + const preserveRestoreRect = + windowState.restoreRect ?? + ({ + x: windowState.x, + y: windowState.y, + width: windowState.width, + height: windowState.height, + } satisfies WindowRect); + + return { + ...windowState, + ...snappedRect, + z: nextZ, + maximized: zone === "top", + snapped: zone === "top" ? null : zone, + restoreRect: preserveRestoreRect, + }; + }), + ); + + setFocusedWindowId(windowId); + onSwitchTab(target.tab); + + // Surface sync: snapped rect + if (target.surfaceId) { + void surfaceUpdateRef.current({ + surfaceId: target.surfaceId, + state: "open", + rect: snappedRect, + zIndex: nextZ, + }); + } + }, + [onSwitchTab], + ); + + const beginDrag = useCallback( + (event: ReactPointerEvent, windowId: number) => { + if (event.button !== 0) { + return; + } + const target = windowsRef.current.find((windowState) => windowState.id === windowId); + if (!target || target.minimized || target.maximized) { + return; + } + if ((event.target as HTMLElement).closest("[data-window-action]")) { + return; + } + + const bounds = getDesktopBounds(desktopRef.current); + let startRect: WindowRect = { + x: target.x, + y: target.y, + width: target.width, + height: target.height, + }; + + if (target.snapped && target.restoreRect) { + startRect = clampRectToBounds(target.restoreRect, bounds); + setWindows((previous) => + previous.map((windowState) => + windowState.id === windowId + ? { + ...windowState, + ...startRect, + snapped: null, + restoreRect: undefined, + } + : windowState, + ), + ); + } + + focusWindow(windowId); + interactionRef.current = { + type: "drag", + windowId, + startClientX: event.clientX, + startClientY: event.clientY, + startRect, + }; + snapPreviewRef.current = null; + setSnapPreview(null); + document.body.classList.add("os-dragging"); + event.preventDefault(); + }, + [focusWindow], + ); + + const beginResize = useCallback( + ( + event: ReactPointerEvent, + windowId: number, + edge: ResizeEdge, + ) => { + if (event.button !== 0) { + return; + } + const target = windowsRef.current.find((windowState) => windowState.id === windowId); + if (!target || target.minimized || target.maximized) { + return; + } + + focusWindow(windowId); + interactionRef.current = { + type: "resize", + windowId, + edge, + startClientX: event.clientX, + startClientY: event.clientY, + startRect: { + x: target.x, + y: target.y, + width: target.width, + height: target.height, + }, + }; + snapPreviewRef.current = null; + setSnapPreview(null); + document.body.classList.add("os-dragging"); + event.preventDefault(); + event.stopPropagation(); + }, + [focusWindow], + ); + + useEffect(() => { + const onPointerMove = (event: PointerEvent) => { + const interaction = interactionRef.current; + if (!interaction) { + return; + } + + const bounds = getDesktopBounds(desktopRef.current); + const deltaX = event.clientX - interaction.startClientX; + const deltaY = event.clientY - interaction.startClientY; + + if (interaction.type === "drag") { + setWindows((previous) => + previous.map((windowState) => { + if (windowState.id !== interaction.windowId) { + return windowState; + } + + const nextRect = clampRectToBounds( + { + ...interaction.startRect, + x: interaction.startRect.x + deltaX, + y: interaction.startRect.y + deltaY, + }, + bounds, + ); + + return { + ...windowState, + x: nextRect.x, + y: nextRect.y, + }; + }), + ); + + const desktopClientBounds = getDesktopClientBounds(desktopRef.current); + const snapZone = detectSnapZone( + event.clientX, + event.clientY, + desktopClientBounds, + ); + const nextPreview = snapZone + ? { + zone: snapZone, + rect: clampRectToBounds(getSnapRect(snapZone, bounds), bounds), + } + : null; + if (snapPreviewRef.current?.zone !== nextPreview?.zone) { + snapPreviewRef.current = nextPreview; + setSnapPreview(nextPreview); + } + } else { + setWindows((previous) => + previous.map((windowState) => { + if (windowState.id !== interaction.windowId) { + return windowState; + } + + const nextRect = resizeRect( + interaction.startRect, + interaction.edge, + deltaX, + deltaY, + bounds, + ); + return { ...windowState, ...nextRect }; + }), + ); + } + }; + + const stopInteraction = () => { + const interaction = interactionRef.current; + if (!interaction) { + return; + } + + interactionRef.current = null; + document.body.classList.remove("os-dragging"); + + const preview = snapPreviewRef.current; + snapPreviewRef.current = null; + setSnapPreview(null); + + if (interaction.type === "drag" && preview) { + snapWindow(interaction.windowId, preview.zone); + // snapWindow handles its own surface sync + } else { + // Drag without snap, or resize: sync the settled rect + const settled = windowsRef.current.find( + (w) => w.id === interaction.windowId, + ); + if (settled?.surfaceId) { + void surfaceUpdateRef.current({ + surfaceId: settled.surfaceId, + rect: { x: settled.x, y: settled.y, width: settled.width, height: settled.height }, + zIndex: settled.z, + }); + } + } + }; + + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", stopInteraction); + window.addEventListener("pointercancel", stopInteraction); + return () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", stopInteraction); + window.removeEventListener("pointercancel", stopInteraction); + document.body.classList.remove("os-dragging"); + }; + }, [snapWindow]); + + useEffect(() => { + const onResize = () => { + const bounds = getDesktopBounds(desktopRef.current); + setWindows((previous) => + previous.map((windowState) => { + if (windowState.maximized) { + return { + ...windowState, + x: 0, + y: 0, + width: bounds.width, + height: bounds.height, + }; + } + + const nextRect = clampRectToBounds(windowState, bounds); + return { ...windowState, ...nextRect }; + }), + ); + }; + + window.addEventListener("resize", onResize); + return () => { + window.removeEventListener("resize", onResize); + }; + }, []); + + useEffect(() => { + preloadTabView(tab); + const existing = windowsRef.current + .filter((windowState) => windowState.tab === tab) + .sort((left, right) => right.z - left.z)[0]; + if (existing) { + if (focusedWindowIdRef.current !== existing.id || existing.minimized) { + focusWindow(existing.id, false); + } + return; + } + openWindow(tab, { syncTab: false }); + }, [focusWindow, openWindow, tab]); + + // ── A3: Inbound surface reconciliation ── + // When surfaces arrive from other clients or agents, create/destroy local windows. + useEffect(() => { + const currentWindows = windowsRef.current; + const localSurfaceIds = new Set( + currentWindows + .filter((w) => w.surfaceId) + .map((w) => w.surfaceId as string), + ); + const remoteSurfaceIds = new Set(Object.keys(storeSurfaces)); + + // 1. Surfaces that were removed externally → close matching local windows + for (const win of currentWindows) { + if (win.surfaceId && !remoteSurfaceIds.has(win.surfaceId)) { + // Only auto-close if we didn't locally initiate the close (owned check) + if (!ownedSurfaceIdsRef.current.has(win.surfaceId)) { + setWindows((prev) => prev.filter((w) => w.id !== win.id)); + } + } + } + + // 2. New surfaces that we don't have a local window for → create one + const pendingContentRefs = new Set(pendingSurfaceOpensRef.current.values()); + for (const [surfaceId, surface] of Object.entries(storeSurfaces)) { + if (localSurfaceIds.has(surfaceId)) continue; + if (ownedSurfaceIdsRef.current.has(surfaceId)) continue; // we opened it, patch is in flight + if (surface.state === "closed") continue; + // Only create windows for surfaces targeted at this client. + // Surfaces targeted at other clients or display nodes are not ours to render. + if (surface.targetClientId && storeClientId && surface.targetClientId !== storeClientId) continue; + // Skip app surfaces that match a window with a pending surfaceOpen RPC. + // This prevents the race where the store gets the surface before the + // .then() callback patches the window's surfaceId. + if (surface.kind === "app" && pendingContentRefs.has(surface.contentRef)) continue; + + const bounds = getDesktopBounds(desktopRef.current); + const windowId = nextWindowIdRef.current++; + const nextZ = nextZRef.current++; + + const rect: WindowRect = surface.rect + ? clampRectToBounds(surface.rect, bounds) + : clampRectToBounds( + { + x: 36 + (windowId % 7) * 24, + y: 34 + (windowId % 7) * 18, + width: Math.round(bounds.width * 0.68), + height: Math.round(bounds.height * 0.74), + }, + bounds, + ); + + if (surface.kind === "app") { + // App surfaces map to a built-in tab + const surfaceTab = surface.contentRef as Tab; + if (!TAB_LABELS[surfaceTab]) continue; // invalid tab + preloadTabView(surfaceTab); + + setWindows((prev) => [ + ...prev, + { + id: windowId, + tab: surfaceTab, + z: surface.zIndex ?? nextZ, + minimized: surface.state === "minimized", + maximized: false, + snapped: null, + surfaceId, + ...rect, + }, + ]); + } else if (surface.kind === "webview" || surface.kind === "media") { + // Webview/media surfaces render as iframe windows + setWindows((prev) => [ + ...prev, + { + id: windowId, + tab: "chat" as Tab, // fallback tab (unused for rendering, needed for type) + z: surface.zIndex ?? nextZ, + minimized: surface.state === "minimized", + maximized: false, + snapped: null, + surfaceId, + url: toEmbedUrl(surface.contentRef), + surfaceLabel: surface.label, + ...rect, + }, + ]); + } + } + + // 3. Surface state updates (minimized/open) from remote + for (const win of currentWindows) { + if (!win.surfaceId) continue; + if (ownedSurfaceIdsRef.current.has(win.surfaceId)) continue; + const surface = storeSurfaces[win.surfaceId]; + if (!surface) continue; + + const shouldBeMinimized = surface.state === "minimized"; + if (win.minimized !== shouldBeMinimized) { + setWindows((prev) => + prev.map((w) => + w.id === win.id ? { ...w, minimized: shouldBeMinimized } : w, + ), + ); + } + } + }, [storeSurfaces, storeClientId]); + + const commandActions = useMemo(() => { + const actions: CommandAction[] = []; + + for (const windowTab of allTabs.concat(launchTabs.filter(t => !allTabs.includes(t)))) { + const tabLabel = TAB_LABELS[windowTab]; + const count = windowCountByTab[windowTab] ?? 0; + actions.push({ + id: `focus-${windowTab}`, + label: count ? `Focus ${tabLabel}` : `Open ${tabLabel}`, + hint: count ? `${count} window${count > 1 ? "s" : ""} running` : "launch app", + keywords: [windowTab, tabLabel, "focus", "open"], + run: () => { + openWindow(windowTab, { newWindow: false }); + }, + }); + actions.push({ + id: `new-${windowTab}`, + label: `Open new ${tabLabel} window`, + hint: "spawn parallel workspace", + keywords: [windowTab, tabLabel, "new", "window", "duplicate"], + run: () => { + openWindow(windowTab, { newWindow: true }); + }, + }); + } + + if (focusedWindow) { + actions.push({ + id: "close-focused", + label: "Close focused window", + hint: "Cmd/Ctrl + W", + keywords: ["close", "window"], + run: () => closeWindow(focusedWindow.id), + }); + actions.push({ + id: "minimize-focused", + label: "Minimize focused window", + hint: "hide to dock", + keywords: ["minimize", "hide", "window"], + run: () => minimizeWindow(focusedWindow.id), + }); + actions.push({ + id: focusedWindow.maximized ? "restore-focused" : "maximize-focused", + label: focusedWindow.maximized + ? "Restore focused window" + : "Maximize focused window", + hint: focusedWindow.maximized ? "return to previous size" : "fill desktop", + keywords: ["maximize", "restore", "window"], + run: () => { + if (focusedWindow.maximized) { + restoreWindow(focusedWindow.id); + return; + } + toggleWindowMaximized(focusedWindow.id); + }, + }); + actions.push({ + id: "snap-left-focused", + label: "Snap focused window left", + hint: "Shift + Left Arrow", + keywords: ["snap", "left", "window"], + run: () => snapWindow(focusedWindow.id, "left"), + }); + actions.push({ + id: "snap-right-focused", + label: "Snap focused window right", + hint: "Shift + Right Arrow", + keywords: ["snap", "right", "window"], + run: () => snapWindow(focusedWindow.id, "right"), + }); + actions.push({ + id: "snap-top-focused", + label: "Snap focused window full screen", + hint: "Shift + Up Arrow", + keywords: ["snap", "maximize", "top", "window"], + run: () => snapWindow(focusedWindow.id, "top"), + }); + } + + actions.push({ + id: "toggle-theme", + label: "Toggle theme", + hint: "switch light/dark", + keywords: ["theme", "dark", "light"], + run: () => onToggleTheme(), + }); + actions.push({ + id: "shell-brutalist", + label: "Switch shell to hard brutalist", + hint: "square + flat", + keywords: ["shell", "style", "brutalist", "flat", "square"], + run: () => onChangeShellStyle("brutalist"), + }); + actions.push({ + id: "shell-futurist", + label: "Switch shell to neo futurist", + hint: "sleek + restrained glow", + keywords: ["shell", "style", "futurist", "sleek", "modern"], + run: () => onChangeShellStyle("futurist"), + }); + actions.push({ + id: "disconnect", + label: "Disconnect gateway", + hint: "close websocket session", + keywords: ["disconnect", "gateway", "logout"], + run: () => onDisconnect(), + }); + + return actions; + }, [ + closeWindow, + focusedWindow, + launchTabs, + minimizeWindow, + onDisconnect, + onChangeShellStyle, + onToggleTheme, + openWindow, + restoreWindow, + snapWindow, + toggleWindowMaximized, + windowCountByTab, + ]); + + const filteredCommandActions = useMemo(() => { + const query = commandQuery.trim().toLowerCase(); + if (!query) { + return commandActions; + } + return commandActions.filter((action) => { + const corpus = [ + action.label, + action.hint ?? "", + ...(action.keywords ?? []), + ] + .join(" ") + .toLowerCase(); + return corpus.includes(query); + }); + }, [commandActions, commandQuery]); + + const executeCommand = useCallback((action: CommandAction) => { + action.run(); + setCommandOpen(false); + setCommandQuery(""); + setSelectedCommandIndex(0); + }, []); + + useEffect(() => { + setSelectedCommandIndex(0); + }, [commandQuery, commandOpen]); + + useEffect(() => { + if (selectedCommandIndex < filteredCommandActions.length) { + return; + } + setSelectedCommandIndex(Math.max(0, filteredCommandActions.length - 1)); + }, [filteredCommandActions.length, selectedCommandIndex]); + + useEffect(() => { + if (!commandOpen) { + return; + } + const frame = window.requestAnimationFrame(() => { + commandInputRef.current?.focus(); + }); + return () => { + window.cancelAnimationFrame(frame); + }; + }, [commandOpen]); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + const metaOrCtrl = event.metaKey || event.ctrlKey; + const lowerKey = event.key.toLowerCase(); + + if (metaOrCtrl && lowerKey === "k") { + event.preventDefault(); + setCommandOpen(true); + return; + } + + if (commandOpen) { + if (event.key === "Escape") { + event.preventDefault(); + setCommandOpen(false); + setCommandQuery(""); + return; + } + if (event.key === "ArrowDown") { + event.preventDefault(); + setSelectedCommandIndex((current) => { + if (!filteredCommandActions.length) { + return 0; + } + return (current + 1) % filteredCommandActions.length; + }); + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setSelectedCommandIndex((current) => { + if (!filteredCommandActions.length) { + return 0; + } + return (current - 1 + filteredCommandActions.length) % filteredCommandActions.length; + }); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + if (!filteredCommandActions.length) { + return; + } + executeCommand( + filteredCommandActions[selectedCommandIndex] ?? filteredCommandActions[0], + ); + } + return; + } + + if (metaOrCtrl && lowerKey === "w") { + if (focusedWindowIdRef.current === null) { + return; + } + event.preventDefault(); + closeWindow(focusedWindowIdRef.current); + return; + } + + if (isTypingTarget(event.target)) { + return; + } + + if (event.shiftKey && !metaOrCtrl && focusedWindowIdRef.current !== null) { + if (event.key === "ArrowLeft") { + event.preventDefault(); + snapWindow(focusedWindowIdRef.current, "left"); + return; + } + if (event.key === "ArrowRight") { + event.preventDefault(); + snapWindow(focusedWindowIdRef.current, "right"); + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + snapWindow(focusedWindowIdRef.current, "top"); + } + } + }; + + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [ + closeWindow, + commandOpen, + executeCommand, + filteredCommandActions, + selectedCommandIndex, + snapWindow, + ]); + + const onAppClick = useCallback( + (event: ReactMouseEvent, windowTab: Tab) => { + openWindow(windowTab, { + newWindow: shouldOpenNewWindow(event), + }); + }, + [openWindow], + ); + + return ( +
+ {/* ── Wallpaper Background ── */} + + + {/* ── Status Bar (minimal, macOS-style) ── */} +
+
+ GSV + + {connectionStateLabel} +
+
+ + {clockLabel} +
+
+ + {/* ── Desktop (full canvas for windows) ── */} +
+ {snapPreview ? ( +
+ ) : null} + + {visibleWindows.map((windowState) => { + const isUrlWindow = Boolean(windowState.url); + const windowTitle = isUrlWindow + ? (windowState.surfaceLabel ?? "Webview") + : TAB_LABELS[windowState.tab]; + + return ( +
focusWindow(windowState.id)} + > +
beginDrag(event, windowState.id)} + onDoubleClick={() => toggleWindowMaximized(windowState.id)} + > +
+
+
+ {windowTitle} +
+
+ +
+ {isUrlWindow ? ( +
+