diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c70c6852d..8e7679d0f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -293,7 +293,6 @@ crates/mcp-host shared authenticated loopback host for in-process MCP s crates/preview-mcp MCP server exposing the preview browser to the agent crates/orchestrate-mcp MCP server for orchestration tools crates/computer-use-mcp MCP server for desktop automation (macOS, Windows) -crates/voice macOS dictation; explicit unsupported elsewhere ``` Runtime emits semantic events; UI owns their localization and presentation. diff --git a/Cargo.lock b/Cargo.lock index 6ec093588..00ea24def 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.13.2", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.1" @@ -292,9 +314,9 @@ dependencies = [ "jni 0.22.4", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "thiserror 2.0.19", ] @@ -598,18 +620,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-compression" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-io", - "pin-project-lite", -] - [[package]] name = "async-executor" version = "1.14.0" @@ -1068,9 +1078,7 @@ dependencies = [ "bitflags 2.13.2", "cexpr", "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", + "itertools 0.11.0", "proc-macro2", "quote", "regex", @@ -1228,16 +1236,6 @@ dependencies = [ "piper", ] -[[package]] -name = "borsh" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" -dependencies = [ - "bytes", - "cfg_aliases 0.2.2", -] - [[package]] name = "branches" version = "0.4.6" @@ -1316,15 +1314,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - [[package]] name = "cairo-rs" version = "0.18.5" @@ -1684,13 +1673,13 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.2", + "unicode-width 0.1.14", ] [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "gpui_util", "indexmap 2.14.0", @@ -1719,24 +1708,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compression-codecs" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" -dependencies = [ - "bzip2", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" - [[package]] name = "computer-use-mcp" version = "0.1.0" @@ -2014,6 +1985,26 @@ dependencies = [ "libm", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4739a805a62757a83e5654fa3faabec0442666b263bb2287d5a8185bfd953" +dependencies = [ + "bindgen 0.72.1", +] + [[package]] name = "cosmic-text" version = "0.19.0" @@ -2038,6 +2029,29 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2 0.4.3", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2351,6 +2365,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.11.1" @@ -2498,7 +2518,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "proc-macro2", "quote", @@ -2788,6 +2808,26 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -2879,17 +2919,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.14" @@ -2897,7 +2926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2990,9 +3019,6 @@ name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -dependencies = [ - "getrandom 0.3.4", -] [[package]] name = "fax" @@ -3114,7 +3140,6 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ - "fastrand", "futures-core", "futures-sink", "spin 0.9.8", @@ -3223,6 +3248,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "funty" version = "2.0.0" @@ -3572,6 +3606,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gif" version = "0.14.2" @@ -3789,14 +3833,13 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "accesskit", "anyhow", "async-channel", "async-task", "backtrace", - "bindgen 0.72.1", "bitflags 2.13.2", "chrono", "collections", @@ -3816,6 +3859,7 @@ dependencies = [ "image", "inventory", "itertools 0.14.0", + "libc", "log", "lyon", "num_cpus", @@ -3830,7 +3874,7 @@ dependencies = [ "raw-window-handle", "refineable", "regex", - "resvg", + "resvg 0.46.0", "scheduler", "schemars 1.2.2", "seahash", @@ -3843,18 +3887,14 @@ dependencies = [ "sum_tree", "taffy", "thiserror 2.0.19", - "tracing", "ttf-parser", "url", - "usvg", - "util_macros", + "usvg 0.46.0", "uuid", "waker-fn", "web-time", "windows 0.62.2", - "zed-font-kit 0.14.1-zed (git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269)", "zed-scap", - "ztracing", ] [[package]] @@ -3868,7 +3908,7 @@ dependencies = [ "gpui_wgpu", "jni 0.22.4", "log", - "ndk", + "ndk 0.9.0", "parking_lot", "raw-window-handle", "smallvec", @@ -3878,9 +3918,9 @@ dependencies = [ [[package]] name = "gpui-base" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb52b29a8dcdc8d3f595b8b0839c076f237572ab601ba4577d3da8ae085728" +checksum = "3cf698677b253118459f552fe15513858c669d79a25837a94e480db2ae330050" dependencies = [ "aho-corasick", "anyhow", @@ -3911,16 +3951,66 @@ dependencies = [ "smallvec", "smol", "tracing", + "unicode-linebreak", "unicode-segmentation", "web-time", "windows 0.58.0", ] +[[package]] +name = "gpui-component" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8b7def8e74b0d822d3e96f6b28fb5a2c20eb7a83d30d22fa59ca8088d1159b" +dependencies = [ + "anyhow", + "block2 0.6.2", + "chrono", + "core-text", + "cpal", + "enum-iterator", + "gpui-base", + "gpui-component-macros", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "instant", + "itertools 0.13.0", + "log", + "lsp-types", + "markdown", + "notify", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-av-foundation", + "objc2-avf-audio", + "objc2-foundation 0.3.2", + "objc2-speech", + "once_cell", + "paste", + "raw-window-handle", + "regex", + "resvg 0.45.1", + "ropey", + "rust-i18n", + "schemars 1.2.2", + "serde", + "serde_json", + "serde_repr", + "smallvec", + "smol", + "tracing", + "uuid", + "windows 0.58.0", +] + [[package]] name = "gpui-component-macros" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b3574bd6e108b9a09c9d584e60626c9140add6d8fe839bf57627577b7977e" +checksum = "2968cc7f4a2bd8f51b09cfe4f1e77409951b48a0e2d48f372b010e69c3d7fb49" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", @@ -3951,9 +4041,9 @@ dependencies = [ [[package]] name = "gpui-kit-assets" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885a2abd416e29a76ef1553aea06eb2645a9290cf5e1d3fa02d218ecac6d318f" +checksum = "db6c481ca987f1e6f10b1aed4d47f1b301cd1dc6f3016fac93c305f2a4f307c0" dependencies = [ "anyhow", "gpui-pre", @@ -3966,24 +4056,24 @@ dependencies = [ [[package]] name = "gpui-pre" -version = "0.3.7" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +version = "0.3.8" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "gpui", ] [[package]] name = "gpui-pre-macros" -version = "0.3.7" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +version = "0.3.8" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "gpui_macros", ] [[package]] name = "gpui-pre-platform" -version = "0.3.7" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +version = "0.3.8" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "gpui", "gpui_platform", @@ -4041,33 +4131,33 @@ dependencies = [ [[package]] name = "gpui-pre-reqwest-client" -version = "0.3.7" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +version = "0.3.8" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "reqwest_client", ] [[package]] name = "gpui-pre-sum-tree" -version = "0.3.7" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +version = "0.3.8" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "sum_tree", ] [[package]] name = "gpui-pre-web" -version = "0.3.7" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +version = "0.3.8" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "gpui_web", ] [[package]] name = "gpui-wry" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2448441bc9e8415b8128f4e02f534462368c1205ba5a864fd9aa85963f53b478" +checksum = "e76703e4c356fa306a9fb0762105638e6add8664f7c2fdfebe226af12d351acd" dependencies = [ "anyhow", "gpui-pre", @@ -4077,38 +4167,43 @@ dependencies = [ [[package]] name = "gpui_apple" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "anyhow", - "block", + "async-task", + "block2 0.6.2", "cbindgen", - "cocoa 0.26.0", "collections", "core-foundation 0.10.1", + "core-graphics 0.24.0", "core-video", "derive_more 2.1.1", + "dispatch2", "etagere", "foreign-types", "gpui", + "gpui_util", "image", + "libc", "log", + "mach2 0.6.0", "metal", - "objc", "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "parking_lot", ] [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "accesskit", "accesskit_unix", "anyhow", "as-raw-xcb-connection", "ashpd", - "bitflags 2.13.2", "bytemuck", "calloop", "calloop-wayland-source", @@ -4137,7 +4232,6 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "wayland-protocols-wlr", - "x11-clipboard", "x11rb", "xkbcommon", "zed-scap", @@ -4147,13 +4241,11 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "accesskit", "accesskit_macos", "anyhow", - "async-task", - "block", "block2 0.6.2", "cocoa 0.26.0", "collections", @@ -4161,6 +4253,7 @@ dependencies = [ "core-foundation-sys", "core-graphics 0.24.0", "core-text", + "core-video", "ctor", "dispatch2", "foreign-types", @@ -4170,20 +4263,16 @@ dependencies = [ "gpui_util", "image", "itertools 0.14.0", - "libc", "log", - "mach2", - "media", - "metal", "objc", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", - "objc2-screen-capture-kit", "objc2-user-notifications", "parking_lot", "pathfinder_geometry", "raw-window-handle", + "scheduler", "semver", "smallvec", "strum 0.28.0", @@ -4194,7 +4283,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4205,8 +4294,9 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ + "anyhow", "console_error_panic_hook", "gpui", "gpui_linux", @@ -4218,7 +4308,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "schemars 1.2.2", "serde", @@ -4228,7 +4318,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "anyhow", "log", @@ -4238,7 +4328,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "anyhow", "console_error_panic_hook", @@ -4266,7 +4356,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "anyhow", "bytemuck", @@ -4292,7 +4382,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "accesskit", "accesskit_windows", @@ -4309,6 +4399,7 @@ dependencies = [ "parking_lot", "rand 0.9.4", "raw-window-handle", + "scheduler", "smallvec", "uuid", "windows 0.62.2", @@ -4613,12 +4704,10 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "anyhow", - "async-compression", "bytes", - "derive_more 2.1.1", "futures", "http", "http-body", @@ -4633,7 +4722,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "log", "rustls", @@ -4993,7 +5082,7 @@ dependencies = [ "byteorder-lite", "color_quant", "exr", - "gif", + "gif 0.14.2", "image-webp", "moxcms", "num-traits", @@ -5002,8 +5091,8 @@ dependencies = [ "ravif", "rayon", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.1", + "zune-jpeg 0.5.15", ] [[package]] @@ -5016,6 +5105,12 @@ dependencies = [ "quick-error 2.0.1", ] +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "imagesize" version = "0.14.0" @@ -5061,6 +5156,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" +dependencies = [ + "bitflags 2.13.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" @@ -5570,6 +5685,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -5582,6 +5717,17 @@ dependencies = [ "selectors", ] +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "kurbo" version = "0.13.1" @@ -5628,7 +5774,7 @@ dependencies = [ "jni 0.21.1", "kuchikiki", "libc", - "ndk", + "ndk 0.9.0", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", @@ -5673,12 +5819,6 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" -[[package]] -name = "libbz2-rs-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" - [[package]] name = "libc" version = "0.2.189" @@ -5798,7 +5938,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "serde_core", - "value-bag", ] [[package]] @@ -5931,13 +6070,19 @@ dependencies = [ [[package]] name = "mach2" -version = "0.5.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -6076,20 +6221,6 @@ dependencies = [ "socket2", ] -[[package]] -name = "media" -version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" -dependencies = [ - "anyhow", - "bindgen 0.72.1", - "core-foundation 0.10.1", - "core-video", - "foreign-types", - "metal", - "objc", -] - [[package]] name = "memchr" version = "2.8.3" @@ -6363,6 +6494,20 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + [[package]] name = "ndk" version = "0.9.0" @@ -6372,7 +6517,7 @@ dependencies = [ "bitflags 2.13.2", "jni-sys 0.3.1", "log", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -6384,6 +6529,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -6697,6 +6851,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.2", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + [[package]] name = "notify-rust" version = "4.18.0" @@ -6711,6 +6883,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -6942,15 +7123,36 @@ dependencies = [ "block2 0.6.2", "libc", "objc2 0.6.4", - "objc2-cloud-kit", - "objc2-core-data 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image 0.3.2", - "objc2-core-text", - "objc2-core-video", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-av-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", ] [[package]] @@ -7035,16 +7237,6 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-core-text" version = "0.3.2" @@ -7208,35 +7400,35 @@ dependencies = [ ] [[package]] -name = "objc2-screen-capture-kit" +name = "objc2-security" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "block2 0.6.2", + "bitflags 2.13.2", "objc2 0.6.4", - "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2-core-foundation", ] [[package]] -name = "objc2-security" +name = "objc2-security-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" dependencies = [ - "bitflags 2.13.2", "objc2 0.6.4", - "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] -name = "objc2-security-foundation" +name = "objc2-speech" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +checksum = "f9eb609f9d2a25e0f8b76954f3acaa58348ce2a91285fe867643b2224ac4d263" dependencies = [ + "block2 0.6.2", "objc2 0.6.4", + "objc2-avf-audio", "objc2-foundation 0.3.2", ] @@ -7275,7 +7467,6 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "objc2 0.6.4", - "objc2-core-location", "objc2-foundation 0.3.2", ] @@ -7322,6 +7513,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni 0.21.1", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -7626,16 +7840,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "perf" -version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" -dependencies = [ - "collections", - "serde", - "serde_json", -] - [[package]] name = "pharos" version = "0.5.3" @@ -8068,8 +8272,6 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures", - "log", "parking_lot", "pin-project", "pollster 0.2.5", @@ -8297,7 +8499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.118", @@ -8439,7 +8641,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8835,7 +9037,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "derive_refineable", ] @@ -8934,7 +9136,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "anyhow", "bytes", @@ -8948,21 +9150,38 @@ dependencies = [ "zed-reqwest", ] +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + [[package]] name = "resvg" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" dependencies = [ - "gif", + "gif 0.14.2", "image-webp", "log", "pico-args", "rgb", - "svgtypes", + "svgtypes 0.16.1", "tiny-skia", - "usvg", - "zune-jpeg", + "usvg 0.46.0", + "zune-jpeg 0.5.15", ] [[package]] @@ -9275,7 +9494,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9288,7 +9507,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9390,7 +9609,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9474,7 +9693,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "async-task", "backtrace", @@ -9704,15 +9923,6 @@ dependencies = [ "syn 3.0.5", ] -[[package]] -name = "serde_fmt" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_json" version = "1.0.151" @@ -9912,7 +10122,7 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" dependencies = [ - "dirs 6.0.0", + "dirs 5.0.1", ] [[package]] @@ -10110,7 +10320,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" dependencies = [ - "borsh", "serde_core", ] @@ -10344,106 +10553,37 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "heapless", - "log", "rayon", "tracing", "ztracing", ] [[package]] -name = "sval" -version = "2.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d" - -[[package]] -name = "sval_buffer" -version = "2.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.20.0" +name = "svg_fmt" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb" -dependencies = [ - "sval", -] +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" [[package]] -name = "sval_serde" -version = "2.20.0" +name = "svgtypes" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ - "serde_core", - "sval", - "sval_nested", + "kurbo 0.11.3", + "siphasher 1.0.3", ] -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - [[package]] name = "svgtypes" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" dependencies = [ - "kurbo", + "kurbo 0.13.1", "siphasher 1.0.3", ] @@ -10555,7 +10695,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.57.0", + "windows 0.54.0", ] [[package]] @@ -10917,6 +11057,7 @@ dependencies = [ "futures-lite", "gpui-android", "gpui-base", + "gpui-component", "gpui-component-macros", "gpui-kit-assets", "gpui-pre", @@ -10951,7 +11092,6 @@ dependencies = [ "tcode-runtime", "tcode-services", "tcode-traverse", - "tcode-voice", "tempfile", "unicode-segmentation", "unicode-width 0.2.2", @@ -10962,10 +11102,6 @@ dependencies = [ "zed-reqwest", ] -[[package]] -name = "tcode-voice" -version = "0.1.0" - [[package]] name = "tcode-web" version = "0.1.0" @@ -11011,7 +11147,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -11105,7 +11241,7 @@ dependencies = [ "half", "quick-error 2.0.1", "weezl", - "zune-jpeg", + "zune-jpeg 0.5.15", ] [[package]] @@ -11829,12 +11965,6 @@ dependencies = [ "rand 0.10.2", ] -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.20.1" @@ -12026,6 +12156,33 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher 1.0.3", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "usvg" version = "0.46.0" @@ -12036,8 +12193,8 @@ dependencies = [ "data-url", "flate2", "fontdb", - "imagesize", - "kurbo", + "imagesize 0.14.0", + "kurbo 0.13.1", "log", "pico-args", "roxmltree 0.21.1", @@ -12045,7 +12202,7 @@ dependencies = [ "simplecss", "siphasher 1.0.3", "strict-num", - "svgtypes", + "svgtypes 0.16.1", "tiny-skia-path", "unicode-bidi", "unicode-script", @@ -12077,16 +12234,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "util_macros" -version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" -dependencies = [ - "perf", - "quote", - "syn 2.0.118", -] - [[package]] name = "uuid" version = "1.26.1" @@ -12117,42 +12264,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" -dependencies = [ - "erased-serde", - "serde_core", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - [[package]] name = "version-compare" version = "0.2.1" @@ -12690,7 +12801,7 @@ dependencies = [ "libloading", "log", "naga", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -12791,7 +12902,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -12802,11 +12913,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.57.0" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ - "windows-core 0.57.0", + "windows-core 0.54.0", "windows-targets 0.52.6", ] @@ -12878,12 +12989,10 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.57.0" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -12949,17 +13058,6 @@ dependencies = [ "windows-threading 0.2.1", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -12982,17 +13080,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "windows-interface" version = "0.58.0" @@ -13193,6 +13280,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -13598,16 +13694,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "x11-clipboard" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" -dependencies = [ - "libc", - "x11rb", -] - [[package]] name = "x11-dl" version = "2.21.0" @@ -13960,7 +14046,6 @@ source = "git+https://github.com/zed-industries/reqwest.git?rev=33bc764aa15ff7b2 dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", "futures-util", "h2", @@ -13974,7 +14059,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -14161,17 +14245,6 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" -[[package]] -name = "zlog" -version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" -dependencies = [ - "anyhow", - "chrono", - "collections", - "log", -] - [[package]] name = "zmij" version = "1.0.21" @@ -14181,18 +14254,22 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" dependencies = [ "tracing", - "tracing-subscriber", - "zlog", "ztracing_macro", ] [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" +source = "git+https://github.com/longbridge/gpui-fast#3755a552f29744ada535e1e58dbb4b316c4a6943" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-core" @@ -14209,13 +14286,22 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + [[package]] name = "zune-jpeg" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ - "zune-core", + "zune-core 0.5.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cc616581a..3d842995f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ "crates/orchestrate-mcp", "crates/computer-use-mcp", "crates/ui", - "crates/voice", "crates/app", "crates/ios", "crates/android", diff --git a/crates/android/Cargo.toml b/crates/android/Cargo.toml index 0610fd09f..b0cf29024 100644 --- a/crates/android/Cargo.toml +++ b/crates/android/Cargo.toml @@ -9,7 +9,7 @@ name = "tcode_android" crate-type = ["cdylib", "rlib"] [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false } gpui-android = { path = "../platform/gpui-android" } log = "0.4" tcode-client = { path = "../client" } diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 176378630..c4e57085d 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -23,8 +23,8 @@ preview-mcp = { path = "../preview-mcp" } orchestrate-mcp = { path = "../orchestrate-mcp" } computer-use-mcp = { path = "../computer-use-mcp" } mcp-host = { path = "../mcp-host" } -gpui = { package = "gpui-pre", version = "0.3.7" } -gpui_platform = { package = "gpui-pre-platform", version = "0.3.7", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } +gpui = { package = "gpui-pre", version = "0.3.8" } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } env_logger = "0.11" log = "0.4" smol = "2" diff --git a/crates/app/Info.plist b/crates/app/Info.plist new file mode 100644 index 000000000..e9393a9d8 --- /dev/null +++ b/crates/app/Info.plist @@ -0,0 +1,10 @@ + + + + + NSMicrophoneUsageDescription + Tcode uses the microphone for voice input in the message composer. + NSSpeechRecognitionUsageDescription + Tcode transcribes your speech on-device to type messages by voice. + + diff --git a/crates/app/build.rs b/crates/app/build.rs index a520584b9..26c03bb06 100644 --- a/crates/app/build.rs +++ b/crates/app/build.rs @@ -1,4 +1,13 @@ fn main() { + println!("cargo:rerun-if-changed=Info.plist"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { + let plist = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Info.plist"); + println!( + "cargo:rustc-link-arg-bins=-Wl,-sectcreate,__TEXT,__info_plist,{}", + plist.display() + ); + } + println!("cargo:rerun-if-changed=../../assets/icons/app/tcode.rc"); println!("cargo:rerun-if-changed=../../assets/icons/app/tcode.ico"); diff --git a/crates/ios/Cargo.toml b/crates/ios/Cargo.toml index 0e0a61cb3..e43184778 100644 --- a/crates/ios/Cargo.toml +++ b/crates/ios/Cargo.toml @@ -9,7 +9,7 @@ name = "tcode_ios" crate-type = ["staticlib", "rlib"] [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false } gpui-ios = { path = "../platform/gpui-ios" } log = "0.4" tcode-client = { path = "../client" } diff --git a/crates/platform/gpui-android/Cargo.toml b/crates/platform/gpui-android/Cargo.toml index 6190b7a66..1e7f8cbf6 100644 --- a/crates/platform/gpui-android/Cargo.toml +++ b/crates/platform/gpui-android/Cargo.toml @@ -7,7 +7,7 @@ publish = false description = "Android platform backend for gpui-fast (Vulkan via gpui_wgpu)" [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false } gpui_wgpu = { git = "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/longbridge/gpui-fast" } anyhow = "1" log = "0.4" diff --git a/crates/platform/gpui-android/src/android/window.rs b/crates/platform/gpui-android/src/android/window.rs index ebab83f86..964fd2b80 100644 --- a/crates/platform/gpui-android/src/android/window.rs +++ b/crates/platform/gpui-android/src/android/window.rs @@ -4,10 +4,10 @@ use android_activity::{ input::{KeyAction, Keycode, MotionAction}, }; use gpui::{ - Bounds, Capslock, DevicePixels, DispatchEventResult, Edges, GpuSpecs, KeyUpEvent, Keystroke, - Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, - PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, Scene, Size, - TextInputConfiguration, TextInputStateChange, TouchEvent, TouchId, TouchPhase, + Bounds, Capslock, DevicePixels, DispatchEventResult, Edges, FrameRequestSource, GpuSpecs, + KeyUpEvent, Keystroke, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, + Scene, Size, TextInputConfiguration, TextInputStateChange, TouchEvent, TouchId, TouchPhase, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowInsets, WindowVisibility, point, px, size, }; @@ -636,6 +636,11 @@ impl AndroidWindow { } pub(crate) fn pump_frame(&self, force: bool) { + let signal_source = if force { + FrameRequestSource::NativeCallback + } else { + FrameRequestSource::LocalSchedule + }; let force = force || self.0.forced_frame_requested.replace(false); if !force && !self.0.frame_requested.replace(false) { return; @@ -646,6 +651,8 @@ impl AndroidWindow { callback(RequestFrameOptions { require_presentation: true, force_render: force, + signal_source, + ..Default::default() }); self.0.callbacks.borrow_mut().request_frame = Some(callback); } @@ -1007,7 +1014,7 @@ impl PlatformWindow for AndroidWindow { } fn gpu_specs(&self) -> Option { - Some(self.0.state.borrow().renderer.gpu_specs()) + self.0.state.borrow().renderer.gpu_specs() } fn update_ime_position(&self, _bounds: Bounds) {} diff --git a/crates/platform/gpui-ios/Cargo.toml b/crates/platform/gpui-ios/Cargo.toml index a4aa2e1ae..c116269da 100644 --- a/crates/platform/gpui-ios/Cargo.toml +++ b/crates/platform/gpui-ios/Cargo.toml @@ -7,7 +7,7 @@ publish = false description = "iOS platform backend for gpui-fast (Metal via gpui_wgpu)" [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false } gpui_wgpu = { git = "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/longbridge/gpui-fast" } anyhow = "1" log = "0.4" diff --git a/crates/platform/gpui-ios/src/ios/window.rs b/crates/platform/gpui-ios/src/ios/window.rs index 797b8adf0..39914a9a4 100644 --- a/crates/platform/gpui-ios/src/ios/window.rs +++ b/crates/platform/gpui-ios/src/ios/window.rs @@ -2,12 +2,12 @@ use super::{IosDisplay, raw_handles::IosRawHandles}; use gpui::{ - Bounds, Capslock, DevicePixels, DispatchEventResult, GpuSpecs, KeyDownEvent, KeyUpEvent, - Keystroke, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - Scene, Size, TextInputConfiguration, TextInputStateChange, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowInsets, WindowParams, - WindowVisibility, px, size, + Bounds, Capslock, DevicePixels, DispatchEventResult, FrameRequestSource, GpuSpecs, + KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, + PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, + RequestFrameOptions, Scene, Size, TextInputConfiguration, TextInputStateChange, + WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowInsets, + WindowParams, WindowVisibility, px, size, }; use gpui_wgpu::{GpuContext, WgpuContext, WgpuRenderer, WgpuSurfaceConfig, wgpu}; use raw_window_handle::{ @@ -304,6 +304,12 @@ impl IosWindow { callback(RequestFrameOptions { require_presentation: true, force_render, + signal_source: if force_render { + FrameRequestSource::LocalSchedule + } else { + FrameRequestSource::NativeCallback + }, + ..Default::default() }); } if self.request_frame_callback.borrow().is_none() { @@ -689,7 +695,7 @@ impl PlatformWindow for IosWindow { } fn gpu_specs(&self) -> Option { - Some(self.renderer.borrow().gpu_specs()) + self.renderer.borrow().gpu_specs() } fn update_ime_position(&self, _bounds: Bounds) {} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 53a390e58..0542238aa 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -43,8 +43,8 @@ native-preview = [ "dep:windows-core", "dep:tempfile", ] -# macOS 26 dictation in the composer. -voice = ["dep:tcode-voice"] +# System dictation in the desktop composer. +voice = ["dep:gpui-component"] # macOS TCC permission status, grant and relaunch for a *local* attachment. local-permissions = ["dep:computer-use-mcp"] # Reading an image off the macOS AppKit pasteboard. @@ -58,10 +58,11 @@ tcode-client = { path = "../client" } tcode-core = { path = "../core", default-features = false } tcode-protocol = { path = "../protocol", default-features = false } qrcode = { version = "0.14", default-features = false } -gpui = { package = "gpui-pre", version = "0.3.7" } -gpui-base = "0.7.0" -gpui-component-assets = { package = "gpui-kit-assets", version = "0.7.0" } -gpui-component-macros = "0.7.0" +gpui = { package = "gpui-pre", version = "0.3.8" } +gpui-base = "0.7.1" +gpui-component = { version = "0.7.1", features = ["speech"], optional = true } +gpui-component-assets = { package = "gpui-kit-assets", version = "0.7.1" } +gpui-component-macros = "0.7.1" image = { version = "0.25.10", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp", "tiff"] } base64 = "0.23" async-channel = "2" @@ -90,14 +91,14 @@ web-time = "1" # iOS off), so it is left to the composition roots. [target.'cfg(not(target_family = "wasm"))'.dependencies] tcode-traverse = { path = "../traverse", default-features = false, optional = true } -reqwest-client = { package = "gpui-pre-reqwest-client", version = "=0.3.7" } +reqwest-client = { package = "gpui-pre-reqwest-client", version = "=0.3.8" } # Android apps do not have the Unix certificate files used by native-roots discovery. reqwest = { package = "zed-reqwest", git = "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/zed-industries/reqwest.git", rev = "33bc764aa15ff7b200bf7c93bd96e24878d53e14", default-features = false, features = ["rustls-tls-webpki-roots"] } # The preview backend exists only where a system webview can be embedded as a # child of the GPUI window, so its whole dependency set is target-scoped. [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -gpui-wry = { version = "0.7.0", optional = true } +gpui-wry = { version = "0.7.1", optional = true } wry = { version = "0.53.3", package = "lb-wry", features = ["mac-proxy"], optional = true } raw-window-handle = { version = "0.6", features = ["std"] } # Only the Windows WebContext needs it, but Cargo has no per-feature target @@ -105,7 +106,6 @@ raw-window-handle = { version = "0.6", features = ["std"] } async-lock = { version = "3", optional = true } [target.'cfg(target_os = "macos")'.dependencies] -tcode-voice = { path = "../voice", optional = true } computer-use-mcp = { path = "../computer-use-mcp", optional = true } block2 = { version = "0.6.2", optional = true } objc2 = "0.6.4" @@ -137,8 +137,8 @@ windows-core = { version = "0.61", optional = true } [dev-dependencies] criterion = "0.8" -gpui = { package = "gpui-pre", version = "0.3.7", features = ["test-support"] } -gpui_platform = { package = "gpui-pre-platform", version = "0.3.7", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } +gpui = { package = "gpui-pre", version = "0.3.8", features = ["test-support"] } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } smol = "2" tcode-runtime = { path = "../runtime", features = ["test-support"] } # dump-create is only needed by examples/sanitize_syntaxes.rs and the syntax-set diff --git a/crates/ui/src/composer/components/mod.rs b/crates/ui/src/composer/components/mod.rs index cc7a6ac63..ff3224e28 100644 --- a/crates/ui/src/composer/components/mod.rs +++ b/crates/ui/src/composer/components/mod.rs @@ -7,7 +7,5 @@ mod plan; mod queue_strip; mod trigger_menu; mod user_input; -/// Dictation exists only where the engine does, so the whole component — -/// button included — compiles out elsewhere. -#[cfg(all(feature = "voice", target_os = "macos"))] +#[cfg(feature = "voice")] pub(super) mod voice; diff --git a/crates/ui/src/composer/components/voice.rs b/crates/ui/src/composer/components/voice.rs index c26f03518..b77e71c4f 100644 --- a/crates/ui/src/composer/components/voice.rs +++ b/crates/ui/src/composer/components/voice.rs @@ -1,106 +1,112 @@ -//! Live dictation in the composer: the mic toggle button plus the transcript -//! insertion state machine that keeps rewriting the engine's hypothesis in -//! place. macOS only — see `crates/voice` for the engine side. +//! System dictation and replacement of the session's whole transcript. use super::super::*; - use gpui::SharedString; -use tcode_voice::{DictationEvent, DictationSession}; +#[cfg(any(target_os = "macos", target_os = "windows"))] +use gpui_component::speech::SystemRecognizer; +use gpui_component::speech::{SpeechEvent, SpeechState, SpeechStatus}; -/// The composer's dictation state. pub(in super::super) struct Voice { - /// `tcode_voice::is_supported()`, sampled once per composer: the answer - /// cannot change while the app runs, and the mic button is absent when it - /// is false. - supported: bool, - session: Option, + state: Entity, + locale: &'static str, + insertion: Option, } -impl Voice { - pub(in super::super) fn new() -> Self { - Self { - supported: tcode_voice::is_supported(), - session: None, - } - } -} - -/// A running session and the byte bookkeeping for its insertion point. The -/// transcript occupies `anchor .. anchor + committed_len + volatile_len`; each -/// `Volatile` rewrites the trailing volatile slice, each `Final` turns it into -/// committed text. -struct Dictation { - session: DictationSession, - /// False between `start` and the engine's `Ready` — the first session may - /// still be downloading speech assets, so the button shows a busy state. - ready: bool, - /// True after a graceful stop: capture is over but the pump stays alive so - /// the engine's finalization flush still lands in the editor; the session - /// is dropped when the terminal event arrives. - stopping: bool, - /// Smoothed microphone level (`0.0..=1.0`) shown as a glow behind the mic - /// icon so the user can see their speech is being picked up. - level: f32, - /// Cursor offset (UTF-8 bytes) captured when dictation started. +struct Insertion { anchor: usize, - committed_len: usize, - volatile_len: usize, - /// The input value we last wrote. Any change away from it is a user edit, - /// which ends the session. + last_len: usize, expected: String, - /// Pumps engine events (delivered on the engine's own threads) into the UI - /// thread; dropped with the session. - _events: Task<()>, } -/// Where a `len`-byte chunk goes and what the counters become: every chunk -/// replaces the volatile tail, and a final one turns it into committed text. -/// Offsets are UTF-8 bytes, as `TextareaState` selections are. -fn transcript_edit( - anchor: usize, - committed_len: usize, - volatile_len: usize, - len: usize, - commit: bool, -) -> (std::ops::Range, usize, usize) { - let start = anchor + committed_len; - let replaced = start..start + volatile_len; - if commit { - (replaced, committed_len + len, 0) +fn speech_locale() -> &'static str { + if rust_i18n::locale().starts_with("zh") { + "zh-CN" } else { - (replaced, committed_len, len) + "en-US" + } +} + +fn speech_state(cx: &mut Context) -> SpeechState { + let state = SpeechState::new(cx); + // On Windows the system recognizer uses Microsoft's online speech service. + #[cfg(any(target_os = "macos", target_os = "windows"))] + let state = state.recognizer(SystemRecognizer::new().locale(speech_locale())); + state +} + +impl Voice { + pub(in super::super) fn new( + window: &mut Window, + cx: &mut Context, + subscriptions: &mut Vec, + ) -> Self { + let state = cx.new(speech_state); + subscriptions.push( + cx.subscribe_in(&state, window, |this, _, event, window, cx| { + this.on_dictation_event(event, window, cx); + }), + ); + subscriptions.push(cx.observe(&state, |_, _, cx| cx.notify())); + subscriptions.push(cx.on_release(|this, cx| { + this.voice.insertion = None; + this.voice.state.update(cx, |state, cx| state.cancel(cx)); + })); + Self { + state, + locale: speech_locale(), + insertion: None, + } } } impl Composer { - /// The mic toggle, or `None` when this machine has no dictation engine. - pub(in super::super) fn render_mic_button(&self, cx: &mut Context) -> Option { - if !self.voice.supported { + pub(in super::super) fn render_mic_button( + &mut self, + cx: &mut Context, + ) -> Option { + let locale = speech_locale(); + if locale != self.voice.locale { + self.abort_dictation(cx); + self.voice.state.update(cx, |state, cx| { + // Preserve session ids so a previous sink cannot address a new session. + let previous = std::mem::replace(state, SpeechState::new(cx)); + #[cfg(any(target_os = "macos", target_os = "windows"))] + let previous = previous.recognizer(SystemRecognizer::new().locale(locale)); + *state = previous; + }); + self.voice.locale = locale; + } + let state = self.voice.state.read(cx); + if !state.has_recognizer() { return None; } - let dictation = self.voice.session.as_ref(); - let preparing = dictation.is_some_and(|d| !d.ready || d.stopping); - let tooltip: SharedString = match dictation { - None => crate::tr!("composer.voice_start"), - Some(_) if preparing => crate::tr!("composer.voice_preparing"), - Some(_) => crate::tr!("composer.voice_stop"), + let status = state.status(); + let preparing = matches!(status, SpeechStatus::Connecting | SpeechStatus::Stopping); + let available = state.is_available(cx); + let tooltip: SharedString = if !available { + crate::tr!("composer.voice_unavailable") + } else if preparing { + crate::tr!("composer.voice_preparing") + } else if status.is_active() { + crate::tr!("composer.voice_stop") + } else { + crate::tr!("composer.voice_start") } .into_owned() .into(); - // Recording tints the glyph with the danger accent, matching the stop - // button's "something is live" idiom; preparing stays muted. - let color = if dictation.is_some() && !preparing { + let color = if status == SpeechStatus::Recording { cx.theme().danger } else { cx.theme().muted_foreground }; - + // Kit SpeechButton/SpeechWaveform require gpui-component's theme global, which Tcode does not initialise. Some( Button::new("voice-mic") .ghost() .compact() .h(px(28.)) .rounded(crate::material::radius_chip(cx)) + .disabled(!available) .tooltip(tooltip) .child(if preparing { Spinner::new().small().color(color).into_any_element() @@ -109,118 +115,47 @@ impl Composer { .path("icons/mic.svg") .small() .text_color(color); - match dictation { - Some(d) => div() + if status == SpeechStatus::Recording { + div() .rounded_full() .p(px(2.)) - .bg(cx.theme().danger.opacity(0.08 + d.level * 0.42)) + .bg(cx + .theme() + .danger + .opacity(0.08 + state.levels().last().unwrap_or(0.) * 0.42)) .child(icon) - .into_any_element(), - None => icon.into_any_element(), + .into_any_element() + } else { + icon.into_any_element() } }) - .on_click(cx.listener(|this, _, window, cx| this.toggle_dictation(window, cx))) + .on_click(cx.listener(|this, _, _, cx| { + this.voice.state.update(cx, |state, cx| state.toggle(cx)); + })) .into_any_element(), ) } - /// Click handler: a second click stops (and finalizes) the running session. - pub(in super::super) fn toggle_dictation( - &mut self, - window: &mut Window, - cx: &mut Context, - ) { - if self.stop_dictation(cx) { - return; - } - let locale = rust_i18n::locale(); - let (events_tx, events_rx) = async_channel::unbounded(); - let session = match tcode_voice::start( - tcode_voice::preferred_locale(locale.as_ref()), - Box::new(move |event| { - let _ = events_tx.try_send(event); - }), - ) { - Ok(session) => session, - Err(error) => { - window.push_notification( - Notification::error( - crate::tr!("composer.voice_error", error = error).into_owned(), - ), - cx, - ); - return; - } - }; - - let (anchor, expected) = self.input.update(cx, |state, cx| { - // Focus keeps Escape (handled on the card) reachable and leaves the - // caret where the transcript is about to appear. - state.focus(window, cx); - (state.cursor(), state.value().to_string()) - }); - let events = cx.spawn_in(window, async move |this, cx| { - while let Ok(event) = events_rx.recv().await { - if this - .update_in(cx, |this, window, cx| { - this.on_dictation_event(event, window, cx) - }) - .is_err() - { - break; - } - } - }); - self.voice.session = Some(Dictation { - session, - ready: false, - stopping: false, - level: 0.0, - anchor, - committed_len: 0, - volatile_len: 0, - expected, - _events: events, - }); - cx.notify(); - } - - /// Gracefully stop the running session: capture ends now, but the pump - /// stays alive so the engine's finalization flush still reaches the editor - /// before `Ended` clears the state. Returns whether a session existed - /// (Escape uses this to claim the keystroke). pub(in super::super) fn stop_dictation(&mut self, cx: &mut Context) -> bool { - let Some(dictation) = self.voice.session.as_mut() else { - return false; - }; - if !dictation.stopping { - dictation.stopping = true; - dictation.session.stop(); - cx.notify(); + let active = self.voice.state.read(cx).status().is_active(); + if active { + self.voice.state.update(cx, |state, cx| state.stop(cx)); } - true + active } - /// Drop the session immediately, discarding any pending finalization. For - /// paths where a late flush would land in the wrong place: submit clears - /// the editor, destination switches change it, and user edits invalidate - /// the anchor. pub(in super::super) fn abort_dictation(&mut self, cx: &mut Context) { - if self.voice.session.take().is_some() { - cx.notify(); - } + self.voice.insertion = None; + self.voice.state.update(cx, |state, cx| state.cancel(cx)); } - /// Typing, pasting or undoing while dictating ends the session in place: - /// the anchor no longer describes the text, and merging a live hypothesis - /// with concurrent edits is not worth the ambiguity. pub(in super::super) fn stop_dictation_on_user_edit(&mut self, cx: &mut Context) { - let value = self.input.read(cx).value(); - if self - .voice - .session - .as_ref() - .is_some_and(|dictation| dictation.expected != value.as_ref()) + if self.voice.state.read(cx).status().is_active() + && self + .voice + .insertion + .as_ref() + .is_none_or(|insertion| insertion.expected != self.input.read(cx).value().as_ref()) { self.abort_dictation(cx); } @@ -228,103 +163,232 @@ impl Composer { fn on_dictation_event( &mut self, - event: DictationEvent, + event: &SpeechEvent, window: &mut Window, cx: &mut Context, ) { - match &event { - DictationEvent::Level(_) => {} - other => log::info!("dictation event: {other:?}"), - } match event { - DictationEvent::Ready => { - if let Some(dictation) = self.voice.session.as_mut() { - dictation.ready = true; - cx.notify(); + SpeechEvent::Started => { + // A cancelled session can have a Started event already queued. + if self.voice.state.read(cx).status().is_active() { + let (anchor, expected) = self.input.update(cx, |input, cx| { + input.focus(window, cx); + (input.cursor(), input.value().to_string()) + }); + self.voice.insertion = Some(Insertion { + anchor, + last_len: 0, + expected, + }); } } - DictationEvent::Level(level) => { - if let Some(dictation) = self.voice.session.as_mut() { - // Fast attack, slow decay: peaks register instantly and - // fade out instead of flickering at the callback rate. - dictation.level = level.max(dictation.level * 0.8); - cx.notify(); + SpeechEvent::Partial(text) | SpeechEvent::Final(text) => { + self.insert_transcript(text.clone(), window, cx); + if matches!(event, SpeechEvent::Final(_)) { + self.voice.insertion = None; } } - DictationEvent::Volatile(text) => self.insert_transcript(text, false, window, cx), - DictationEvent::Final(text) => self.insert_transcript(text, true, window, cx), - DictationEvent::Error(message) => { - self.stop_dictation(cx); + SpeechEvent::Error(error) => { + self.voice.insertion = None; window.push_notification( Notification::error( - crate::tr!("composer.voice_error", error = message).into_owned(), + crate::tr!("composer.voice_error", error = error).into_owned(), ), cx, ); } - DictationEvent::Ended => { - self.voice.session = None; - cx.notify(); - } + SpeechEvent::Cancelled => self.voice.insertion = None, } + cx.notify(); } - /// Replace the current volatile range with `text`, committing it when the - /// engine says it is final. fn insert_transcript( &mut self, - text: String, - commit: bool, + text: SharedString, window: &mut Window, cx: &mut Context, ) { - let input = self.input.clone(); - let Some(dictation) = self.voice.session.as_mut() else { + let Some(insertion) = self.voice.insertion.as_mut() else { return; }; - let (range, committed_len, volatile_len) = transcript_edit( - dictation.anchor, - dictation.committed_len, - dictation.volatile_len, - text.len(), - commit, - ); - input.update(cx, |state, cx| { - state.set_selected_range(range, cx); - state.replace(text, window, cx); + if self.input.read(cx).value().as_ref() != insertion.expected { + self.abort_dictation(cx); + return; + } + let range = insertion.anchor..insertion.anchor + insertion.last_len; + self.input.update(cx, |input, cx| { + input.set_selected_range(range, cx); + input.replace(text.clone(), window, cx); }); - dictation.committed_len = committed_len; - dictation.volatile_len = volatile_len; - // The replacement emits `Change` like any edit; record the result so - // the edit guard can tell our own writes from the user's. - dictation.expected = input.read(cx).value().to_string(); + insertion.last_len = text.len(); + // Change events run after the update; our writes must match before delivery. + insertion.expected = self.input.read(cx).value().to_string(); } } #[cfg(test)] mod tests { - use super::transcript_edit; + use super::*; + use gpui::{TestAppContext, size}; + use gpui_component::speech::{ + AudioFormat, AudioInput, AudioSink, RecognitionSession, SpeechError, SpeechRecognizer, + SpeechSink, + }; + use std::cell::RefCell; + + struct Recognizer(Rc>>); + struct Recognition; + + impl SpeechRecognizer for Recognizer { + fn start( + &self, + sink: SpeechSink, + cx: &mut App, + ) -> Result, SpeechError> { + sink.ready(cx); + *self.0.borrow_mut() = Some(sink); + Ok(Box::new(Recognition)) + } + } - /// A hypothesis is rewritten in place until it is finalized, after which - /// the next one starts where the committed text ends. - #[test] - fn each_chunk_replaces_only_the_volatile_tail() { - let (anchor, mut committed, mut volatile) = (5, 0, 0); + impl RecognitionSession for Recognition { + fn push_audio(&mut self, _: &[i16], _: &mut App) {} + fn finish(&mut self, _: &mut App) {} + } - let (range, c, v) = transcript_edit(anchor, committed, volatile, 2, false); - assert_eq!(range, 5..5); - (committed, volatile) = (c, v); + struct SilentInput; + impl AudioInput for SilentInput { + fn start( + &self, + _: AudioFormat, + _: AudioSink, + _: &mut App, + ) -> Result { + Ok(Subscription::new(|| {})) + } + } - let (range, c, v) = transcript_edit(anchor, committed, volatile, 6, false); - assert_eq!(range, 5..7); - (committed, volatile) = (c, v); + #[gpui::test] + fn dictation_revises_the_draft_and_cannot_overwrite_edits_or_submission( + cx: &mut TestAppContext, + ) { + let _locale_guard = crate::settings::TestLocaleGuard::acquire(); + cx.update(crate::theme::init); + let host = tcode_runtime::pipe::spawn_host( + tcode_services::store::SessionStore::open_at(std::env::temp_dir().join(format!( + "tcode-dictation-test-{}-{}", + std::process::id(), + tcode_services::store::now_millis() + ))) + .unwrap(), + tcode_runtime::pipe::HostServices::default(), + ) + .unwrap(); + let (session_id, timeline) = smol::block_on(host.update_state_for_test(|state, cx| { + let id = state.start_draft("dictation".into(), std::env::temp_dir(), cx); + let timeline = state.residents.live[&id].timeline.clone(); + (id, timeline) + })) + .unwrap(); + let store = cx.new(|cx| WorkspaceStore::new(host.link(), cx)); + store.update(cx, |store, cx| { + store.set_session_replica_for_test(session_id, timeline, cx); + }); + let sink = Rc::new(RefCell::new(None::)); + let (composer, cx) = cx.add_window_view(|window, cx| { + let composer = Composer::new(store.clone(), window, cx); + composer.voice.state.update(cx, |state, cx| { + *state = SpeechState::new(cx) + .recognizer(Recognizer(sink.clone())) + .input(SilentInput); + }); + composer + }); + cx.simulate_resize(size(px(800.), px(600.))); + cx.update(|window, cx| window.draw(cx).clear(cx)); + composer.update_in(cx, |composer, window, cx| { + composer.set_draft("前后", window, cx); + composer.input.update(cx, |input, cx| { + input.set_selected_range(3..3, cx); + }); + }); + composer.update_in(cx, |composer, _, cx| { + composer + .voice + .state + .update(cx, |state, cx| state.toggle(cx)); + }); + let first = sink.borrow().clone().unwrap(); + for (text, expected) in [ + ("hello brave world", "前hello brave world后"), + ("hi", "前hi后"), + ("", "前后"), + ("你好", "前你好后"), + ] { + cx.update(|_, cx| first.hypothesis(text, cx)); + assert_eq!( + composer.read_with(cx, |composer, cx| composer.draft(cx)), + expected + ); + } + cx.update(|_, cx| first.finish(cx)); + assert_eq!( + composer.read_with(cx, |composer, cx| composer.draft(cx)), + "前你好后" + ); - let (range, c, v) = transcript_edit(anchor, committed, volatile, 6, true); - assert_eq!(range, 5..11); - assert_eq!((c, v), (6, 0)); - (committed, volatile) = (c, v); + composer.update_in(cx, |composer, _, cx| { + composer + .voice + .state + .update(cx, |state, cx| state.toggle(cx)); + }); + let edited = sink.borrow().clone().unwrap(); + cx.update(|_, cx| edited.hypothesis(" text", cx)); + cx.simulate_input("!"); + assert_eq!( + composer.read_with(cx, |composer, cx| composer.voice.state.read(cx).status()), + SpeechStatus::Idle, + ); + let draft = composer.read_with(cx, |composer, cx| composer.draft(cx)); + assert_eq!(draft, "前你好 text!后"); + cx.update(|_, cx| { + edited.hypothesis("late overwrite", cx); + edited.finish(cx); + }); + assert_eq!( + composer.read_with(cx, |composer, cx| composer.draft(cx)), + draft + ); - let (range, ..) = transcript_edit(anchor, committed, volatile, 3, false); - assert_eq!(range, 11..11); + composer.update_in(cx, |composer, window, cx| { + composer.set_draft("", window, cx); + }); + composer.update_in(cx, |composer, _, cx| { + composer + .voice + .state + .update(cx, |state, cx| state.toggle(cx)); + }); + let submitted = sink.borrow().clone().unwrap(); + cx.update(|_, cx| submitted.hypothesis("/plan", cx)); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("escape"); + assert_eq!( + composer.read_with(cx, |composer, cx| composer.voice.state.read(cx).status()), + SpeechStatus::Stopping, + ); + composer.update_in(cx, |composer, window, cx| { + let input = composer.input.clone(); + composer.submit(&input, false, window, cx); + }); + cx.update(|_, cx| { + submitted.hypothesis("late text", cx); + submitted.finish(cx); + }); + assert_eq!( + composer.read_with(cx, |composer, cx| composer.draft(cx)), + "" + ); } } diff --git a/crates/ui/src/composer/mod.rs b/crates/ui/src/composer/mod.rs index 2f09bea29..182b6ef7e 100644 --- a/crates/ui/src/composer/mod.rs +++ b/crates/ui/src/composer/mod.rs @@ -6,7 +6,7 @@ mod components; pub(crate) mod model; use components::images::PendingImage; -#[cfg(all(feature = "voice", target_os = "macos"))] +#[cfg(feature = "voice")] use components::voice::Voice; use model::*; @@ -203,7 +203,7 @@ pub struct Composer { /// strip contains at least one scheduled row. scheduled_countdown_tick: Option>, /// Mic button + live dictation session (see `components::voice`). - #[cfg(all(feature = "voice", target_os = "macos"))] + #[cfg(feature = "voice")] voice: Voice, _subscriptions: Vec, } @@ -348,7 +348,7 @@ impl Composer { InputEvent::Change => { // An edit that did not come from the transcript writer // ends dictation (see `components::voice`). - #[cfg(all(feature = "voice", target_os = "macos"))] + #[cfg(feature = "voice")] this.stop_dictation_on_user_edit(cx); this.recompute_trigger(cx); cx.notify(); @@ -411,6 +411,13 @@ impl Composer { ), ]; + #[cfg(feature = "voice")] + let (voice, subscriptions) = { + let mut subscriptions = subscriptions; + let voice = Voice::new(window, cx, &mut subscriptions); + (voice, subscriptions) + }; + Self { compact, workspace_store, @@ -446,8 +453,8 @@ impl Composer { image_load_generation: 0, pending_image_loads: 0, scheduled_countdown_tick: None, - #[cfg(all(feature = "voice", target_os = "macos"))] - voice: Voice::new(), + #[cfg(feature = "voice")] + voice, _subscriptions: subscriptions, } } @@ -467,7 +474,7 @@ impl Composer { return; }; // The dictation anchor belongs to the text we are about to swap out. - #[cfg(all(feature = "voice", target_os = "macos"))] + #[cfg(feature = "voice")] self.abort_dictation(cx); let cursor = incoming_text.len(); self.input.update(cx, |state, cx| { @@ -587,6 +594,8 @@ impl Composer { { return; } + #[cfg(feature = "voice")] + self.abort_dictation(cx); // A blocking question uses this text as the current custom answer, // advancing through the same path as an option click. A non-blocking // one leaves the composer to ordinary sends and steers. @@ -729,7 +738,7 @@ impl Composer { window: &mut Window, cx: &mut Context, ) { - #[cfg(all(feature = "voice", target_os = "macos"))] + #[cfg(feature = "voice")] self.abort_dictation(cx); self.text_cache.clear_current(); input.update(cx, |state, cx| state.set_value("", window, cx)); @@ -1075,13 +1084,13 @@ impl Render for Composer { .gap_1() .items_center(); - #[cfg(all(feature = "voice", target_os = "macos"))] + #[cfg(feature = "voice")] let mic = if self.compact || readonly { None } else { self.render_mic_button(cx) }; - #[cfg(not(all(feature = "voice", target_os = "macos")))] + #[cfg(not(feature = "voice"))] let mic: Option = None; // The platform picker is the phone's only way in; desktops paste and @@ -1301,7 +1310,7 @@ impl Render for Composer { } // Escape ends dictation (keeping the transcript) before it can // mean anything else. - #[cfg(all(feature = "voice", target_os = "macos"))] + #[cfg(feature = "voice")] if key == "escape" && this.stop_dictation(cx) { cx.stop_propagation(); return; diff --git a/crates/ui/src/markdown/inline_flow.rs b/crates/ui/src/markdown/inline_flow.rs index 5e532e48b..a739e5c73 100644 --- a/crates/ui/src/markdown/inline_flow.rs +++ b/crates/ui/src/markdown/inline_flow.rs @@ -655,7 +655,7 @@ fn line_ranges( let boundaries = window .text_system() .line_wrapper(text_style.font(), font_size) - .wrap_line(&fragments, wrap_width) + .wrap_line(&fragments, wrap_width, Default::default()) .map(|boundary| boundary.ix.min(total_len)) .collect::>(); let mut ranges = Vec::with_capacity(boundaries.len() + 1); diff --git a/crates/voice/Cargo.toml b/crates/voice/Cargo.toml deleted file mode 100644 index 509979dfd..000000000 --- a/crates/voice/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "tcode-voice" -version = "0.1.0" -edition = "2024" - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(voice_shim)"] } diff --git a/crates/voice/build.rs b/crates/voice/build.rs deleted file mode 100644 index 08a3f0347..000000000 --- a/crates/voice/build.rs +++ /dev/null @@ -1,113 +0,0 @@ -// Build scripts run at compile time on the developer's machine, never inside -// the GUI app, so the console-suppressing process helpers do not apply. -#![allow(clippy::disallowed_methods)] - -use std::{env, fs, path::PathBuf, process::Command}; - -fn command_output(program: &str, args: &[&str]) -> Option { - let output = Command::new(program).args(args).output().ok()?; - output - .status - .success() - .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned()) -} - -fn sdk_is_at_least_26(version: &str) -> bool { - version - .split('.') - .next() - .and_then(|part| part.parse::().ok()) - .is_some_and(|major| major >= 26) -} - -fn main() { - println!("cargo:rerun-if-changed=swift/shim.swift"); - println!("cargo:rerun-if-env-changed=TCODE_VOICE_FORCE_STUB"); - - if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") - || env::var("TCODE_VOICE_FORCE_STUB").as_deref() == Ok("1") - { - return; - } - - let Some(sdk_version) = command_output("xcrun", &["--sdk", "macosx", "--show-sdk-version"]) - else { - return; - }; - if !sdk_is_at_least_26(&sdk_version) { - return; - } - - let sdk_path = command_output("xcrun", &["--sdk", "macosx", "--show-sdk-path"]) - .expect("macOS SDK path is unavailable"); - let swiftc = - command_output("xcrun", &["--find", "swiftc"]).expect("Swift compiler is unavailable"); - let arch = match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { - Ok("aarch64") => "arm64", - Ok("x86_64") => "x86_64", - Ok(other) => panic!("unsupported macOS architecture for tcode-voice: {other}"), - Err(error) => panic!("CARGO_CFG_TARGET_ARCH is unavailable: {error}"), - }; - let target = format!("{arch}-apple-macosx26.0"); - let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is unavailable")); - let library = out_dir.join("libtcode_voice_shim.a"); - let module_cache = out_dir.join("swift-module-cache"); - - // The SDK's concurrency text stub contains `$ld$previous` directives that - // rewrite its install name to @rpath when the final Rust executable keeps - // an older deployment target. The shim itself is macOS 26-only and needs - // the current Swift concurrency ABI, which macOS 26 provides in the dyld - // cache at /usr/lib/swift. Keep the final app's deployment target intact, - // but make its Swift autolink resolve to that system install name. - let concurrency_stub_source = - PathBuf::from(&sdk_path).join("usr/lib/swift/libswift_Concurrency.tbd"); - let mut concurrency_stub = fs::read_to_string(&concurrency_stub_source) - .expect("failed to read the Swift concurrency text stub"); - while let Some(start) = concurrency_stub.find("'$ld$previous") { - let end = concurrency_stub[start..] - .find("',") - .map(|offset| start + offset + 2) - .expect("malformed $ld$previous directive in Swift concurrency text stub"); - concurrency_stub.replace_range(start..end, ""); - } - fs::write(out_dir.join("libswift_Concurrency.tbd"), concurrency_stub) - .expect("failed to write the adjusted Swift concurrency text stub"); - - let status = Command::new(&swiftc) - .args([ - "-parse-as-library", - "-swift-version", - "5", - "-target", - &target, - "-sdk", - &sdk_path, - "-module-cache-path", - ]) - .arg(&module_cache) - .args(["-static", "-emit-library", "swift/shim.swift", "-o"]) - .arg(&library) - .status() - .expect("failed to invoke swiftc for tcode-voice"); - assert!( - status.success(), - "swiftc failed to compile swift/shim.swift" - ); - - println!("cargo:rustc-link-search=native={}", out_dir.display()); - println!("cargo:rustc-link-lib=static=tcode_voice_shim"); - for framework in ["Speech", "AVFoundation", "Foundation", "CoreMedia"] { - println!("cargo:rustc-link-lib=framework={framework}"); - } - - println!("cargo:rustc-link-search=native=/usr/lib/swift"); - println!("cargo:rustc-link-search=native={sdk_path}/usr/lib/swift"); - println!("cargo:rustc-link-search=native={sdk_path}/usr/lib/swift/macosx"); - if let Some(toolchain) = PathBuf::from(swiftc).parent().and_then(|bin| bin.parent()) { - println!( - "cargo:rustc-link-search=native={}", - toolchain.join("lib/swift/macosx").display() - ); - } - println!("cargo:rustc-cfg=voice_shim"); -} diff --git a/crates/voice/examples/file_dictation.rs b/crates/voice/examples/file_dictation.rs deleted file mode 100644 index 1e58bcc69..000000000 --- a/crates/voice/examples/file_dictation.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::{env, process, sync::mpsc}; - -use tcode_voice::DictationEvent; - -fn main() { - let mut args = env::args().skip(1); - let (Some(path), Some(locale), None) = (args.next(), args.next(), args.next()) else { - eprintln!("usage: file_dictation "); - process::exit(2); - }; - - let (sender, receiver) = mpsc::channel(); - // Held for its lifetime: dropping it would cancel the session. - let _session = tcode_voice::start_file( - &locale, - &path, - Box::new(move |event| { - let _ = sender.send(event); - }), - ) - .unwrap_or_else(|error| { - eprintln!("error: {error}"); - process::exit(1); - }); - - while let Ok(event) = receiver.recv() { - match event { - DictationEvent::Ready => println!("ready"), - DictationEvent::Level(_) => {} - DictationEvent::Volatile(text) => println!("volatile: {text}"), - DictationEvent::Final(text) => println!("final: {text}"), - DictationEvent::Error(error) => { - eprintln!("error: {error}"); - process::exit(1); - } - DictationEvent::Ended => { - println!("ended"); - break; - } - } - } -} diff --git a/crates/voice/examples/mic_dictation.rs b/crates/voice/examples/mic_dictation.rs deleted file mode 100644 index 9ab701b4f..000000000 --- a/crates/voice/examples/mic_dictation.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Live microphone dictation smoke test: `cargo run -p tcode-voice --example -//! mic_dictation -- zh_CN [seconds]`. Prints every event with a timestamp. -use std::time::{Duration, Instant}; - -fn main() { - let mut args = std::env::args().skip(1); - let locale = args.next().unwrap_or_else(|| "zh_CN".into()); - let seconds: u64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(8); - let start = Instant::now(); - let session = tcode_voice::start( - &locale, - Box::new(move |event| println!("[{:6.2}s] {event:?}", start.elapsed().as_secs_f32())), - ) - .expect("failed to start dictation"); - std::thread::sleep(Duration::from_secs(seconds)); - println!("[{:6.2}s] calling stop()", start.elapsed().as_secs_f32()); - session.stop(); - std::thread::sleep(Duration::from_secs(3)); -} diff --git a/crates/voice/src/imp.rs b/crates/voice/src/imp.rs deleted file mode 100644 index ad7ad1506..000000000 --- a/crates/voice/src/imp.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::{ - ffi::{CStr, CString, c_char, c_int, c_void}, - panic::{AssertUnwindSafe, catch_unwind}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use super::DictationEvent; - -type EventCallback = unsafe extern "C" fn(*mut c_void, c_int, *const c_char); - -unsafe extern "C" { - fn voice_is_supported(locale: *const c_char) -> bool; - fn voice_start( - locale: *const c_char, - context: *mut c_void, - callback: EventCallback, - ) -> *mut c_void; - fn voice_start_file( - locale: *const c_char, - path: *const c_char, - context: *mut c_void, - callback: EventCallback, - ) -> *mut c_void; - fn voice_stop(session: *mut c_void); - fn voice_free(session: *mut c_void); -} - -struct CallbackState { - callback: Box, - terminal: AtomicBool, -} - -unsafe extern "C" fn event_callback(context: *mut c_void, kind: c_int, text: *const c_char) { - let pointer = context.cast::(); - - // Keep a temporary strong reference for the duration of this callback. The - // raw context owns another reference until the terminal event below. - unsafe { Arc::increment_strong_count(pointer) }; - let state = unsafe { Arc::from_raw(pointer) }; - - if state.terminal.load(Ordering::Acquire) { - return; - } - - let string = || { - if text.is_null() { - String::new() - } else { - unsafe { CStr::from_ptr(text) } - .to_string_lossy() - .into_owned() - } - }; - let (event, terminal) = match kind { - 0 => (DictationEvent::Ready, false), - 1 => (DictationEvent::Volatile(string()), false), - 2 => (DictationEvent::Final(string()), false), - 3 => (DictationEvent::Error(string()), true), - 4 => (DictationEvent::Ended, true), - 5 => match string().parse::() { - Ok(level) => (DictationEvent::Level(level.clamp(0.0, 1.0)), false), - Err(_) => return, - }, - _ => return, - }; - - if terminal - && state - .terminal - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_err() - { - return; - } - - let _ = catch_unwind(AssertUnwindSafe(|| (state.callback)(event))); - - if terminal { - // Release the strong reference transferred to Swift as the context. - drop(unsafe { Arc::from_raw(pointer) }); - } -} - -pub struct Session { - raw: *mut c_void, - _callback: Arc, -} - -// The shim serializes access to its opaque session and accepts stop/free from -// any thread. CallbackState itself contains only Send + Sync data. -unsafe impl Send for Session {} - -impl Session { - pub fn stop(&self) { - unsafe { voice_stop(self.raw) }; - } -} - -impl Drop for Session { - fn drop(&mut self) { - unsafe { voice_free(self.raw) }; - } -} - -pub fn is_supported() -> bool { - let locale = CString::new(super::preferred_locale("en")).expect("locale contains no NUL"); - unsafe { voice_is_supported(locale.as_ptr()) } -} - -pub fn start( - locale: &str, - on_event: Box, -) -> Result { - start_inner(locale, None, on_event) -} - -pub fn start_file( - locale: &str, - path: &str, - on_event: Box, -) -> Result { - start_inner(locale, Some(path), on_event) -} - -fn start_inner( - locale: &str, - path: Option<&str>, - on_event: Box, -) -> Result { - let locale = CString::new(locale).map_err(|_| "locale contains an interior NUL byte")?; - let path = path - .map(CString::new) - .transpose() - .map_err(|_| "audio path contains an interior NUL byte")?; - let callback = Arc::new(CallbackState { - callback: on_event, - terminal: AtomicBool::new(false), - }); - let context = Arc::into_raw(Arc::clone(&callback)).cast_mut().cast(); - let raw = unsafe { - match path.as_ref() { - Some(path) => voice_start_file(locale.as_ptr(), path.as_ptr(), context, event_callback), - None => voice_start(locale.as_ptr(), context, event_callback), - } - }; - - if raw.is_null() { - drop(unsafe { Arc::from_raw(context.cast::()) }); - return Err("failed to create dictation session".into()); - } - - Ok(Session { - raw, - _callback: callback, - }) -} diff --git a/crates/voice/src/lib.rs b/crates/voice/src/lib.rs deleted file mode 100644 index 4949619db..000000000 --- a/crates/voice/src/lib.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Live dictation backed by the macOS 26 SpeechAnalyzer/SpeechTranscriber API. -//! -//! On macOS 26+ (built against an SDK ≥ 26) [`is_supported`] returns true and -//! [`start`] opens a microphone dictation session whose transcript is streamed -//! through [`DictationEvent`]s. Everywhere else the crate compiles to a stub -//! that reports unsupported. -//! -//! Event contract: after `start` returns, the callback receives at most one -//! `Ready`, then any number of `Volatile`/`Final` events, then exactly one -//! terminal event (`Error` or `Ended`). Each `Volatile` replaces the previous -//! volatile text; a `Final` commits text (the current volatile range becomes -//! the final text and a new empty volatile range begins after it). Callbacks -//! arrive on arbitrary non-UI threads. - -/// A transcription event delivered to the callback passed to [`start`]. -#[derive(Debug, Clone)] -pub enum DictationEvent { - /// Speech assets are installed and the microphone is live. - Ready, - /// Microphone input level in `0.0..=1.0`, reported continuously while - /// recording (roughly at the audio callback rate). Purely cosmetic. - Level(f32), - /// In-progress hypothesis; replaces the previous `Volatile` text. - Volatile(String), - /// Finalized text; replaces the current volatile text and is stable. - Final(String), - /// The session failed. Terminal. - Error(String), - /// The session ended after [`DictationSession::stop`] or drop. Terminal. - Ended, -} - -/// A running dictation session. Dropping it cancels recognition. -pub struct DictationSession { - imp: imp::Session, -} - -impl DictationSession { - /// Stop capturing and finalize: remaining audio is decoded, a last - /// `Final` may be delivered, then `Ended`. - pub fn stop(&self) { - self.imp.stop(); - } -} - -/// Whether live dictation is available on this machine (macOS 26+, shim -/// compiled in, and the locale returned by [`preferred_locale`] supported). -pub fn is_supported() -> bool { - imp::is_supported() -} - -/// Map an app locale (`"zh-CN"`, `"en"`, …) to the dictation locale to use. -pub fn preferred_locale(app_locale: &str) -> &'static str { - if app_locale.starts_with("zh") { - "zh_CN" - } else { - "en_US" - } -} - -/// Start a microphone dictation session for `locale` (an identifier such as -/// `"zh_CN"`). Events are delivered on arbitrary threads. -pub fn start( - locale: &str, - on_event: Box, -) -> Result { - imp::start(locale, on_event).map(|imp| DictationSession { imp }) -} - -/// Start transcription from an audio file. -/// -/// This is an implementation hook for examples and integration testing. -#[doc(hidden)] -pub fn start_file( - locale: &str, - path: &str, - on_event: Box, -) -> Result { - imp::start_file(locale, path, on_event).map(|imp| DictationSession { imp }) -} - -#[cfg(all(target_os = "macos", voice_shim))] -mod imp; - -#[cfg(not(all(target_os = "macos", voice_shim)))] -mod imp { - use super::DictationEvent; - - pub struct Session; - - impl Session { - pub fn stop(&self) {} - } - - pub fn is_supported() -> bool { - false - } - - pub fn start( - _locale: &str, - _on_event: Box, - ) -> Result { - Err("dictation is not supported on this platform".into()) - } - - pub fn start_file( - _locale: &str, - _path: &str, - _on_event: Box, - ) -> Result { - Err("dictation is not supported on this platform".into()) - } -} diff --git a/crates/voice/swift/shim.swift b/crates/voice/swift/shim.swift deleted file mode 100644 index d4f56ee6b..000000000 --- a/crates/voice/swift/shim.swift +++ /dev/null @@ -1,498 +0,0 @@ -@preconcurrency import AVFoundation -import CoreMedia -import Foundation -import Speech - -private let eventReady: Int32 = 0 -private let eventVolatile: Int32 = 1 -private let eventFinal: Int32 = 2 -private let eventError: Int32 = 3 -private let eventEnded: Int32 = 4 -private let eventLevel: Int32 = 5 - -public typealias VoiceEventCallback = @convention(c) ( - UnsafeMutableRawPointer?, Int32, UnsafePointer? -) -> Void - -@available(macOS 26.0, *) -private enum VoiceSource: Sendable { - case microphone - case file(URL) -} - -@available(macOS 26.0, *) -private final class AudioFeeder: @unchecked Sendable { - let stream: AsyncStream - - private let continuation: AsyncStream.Continuation - private let lock = NSLock() - private var finished = false - - init() { - (stream, continuation) = AsyncStream.makeStream() - } - - // No explicit bufferStartTime: the analyzer tracks the live input timeline - // itself, and supplying our own timestamps kept it from ever reporting - // results or honoring a targeted finalize. - func feed(_ buffer: AVAudioPCMBuffer) { - lock.lock() - defer { lock.unlock() } - guard !finished else { return } - continuation.yield(AnalyzerInput(buffer: buffer)) - } - - func finish() { - lock.lock() - defer { lock.unlock() } - guard !finished else { return } - finished = true - continuation.finish() - } -} - -@available(macOS 26.0, *) -private actor VoiceCore { - private let localeIdentifier: String - private let source: VoiceSource - private let context: UnsafeMutableRawPointer? - private let callback: VoiceEventCallback - - private var analyzer: SpeechAnalyzer? - private var engine: AVAudioEngine? - private var feeder: AudioFeeder? - private var analysisTask: Task? - private var resultsTask: Task? - private var terminal = false - private var ready = false - private var stopping = false - - init( - localeIdentifier: String, - source: VoiceSource, - context: UnsafeMutableRawPointer?, - callback: @escaping VoiceEventCallback - ) { - self.localeIdentifier = localeIdentifier - self.source = source - self.context = context - self.callback = callback - } - - func run() async { - do { - let requestedLocale = Locale(identifier: localeIdentifier) - guard let locale = await DictationTranscriber.supportedLocale( - equivalentTo: requestedLocale - ) else { - throw VoiceFailure("unsupported speech locale: \(localeIdentifier)") - } - - // DictationTranscriber's progressive preset streams hypotheses - // every few hundred ms; SpeechTranscriber with plain - // volatileResults buffers everything until finalization. - let transcriber = DictationTranscriber( - locale: locale, - preset: .progressiveLongDictation - ) - let modules: [any SpeechModule] = [transcriber] - try await installAssets(for: modules) - guard !stopping else { - await finishAsEnded(cancel: true) - return - } - - let analyzer = SpeechAnalyzer(modules: modules) - self.analyzer = analyzer - startResults(from: transcriber) - - switch source { - case .file(let url): - try await runFile(url: url, analyzer: analyzer) - case .microphone: - try await runMicrophone(analyzer: analyzer, modules: modules) - } - } catch { - if !stopping { - emitTerminal(kind: eventError, text: error.localizedDescription) - } - } - } - - private func installAssets(for modules: [any SpeechModule]) async throws { - var status = await AssetInventory.status(forModules: modules) - guard status != .unsupported else { - throw VoiceFailure("speech assets are unsupported for \(localeIdentifier)") - } - - if status != .installed { - if let request = try await AssetInventory.assetInstallationRequest( - supporting: modules - ) { - try await request.downloadAndInstall() - } - - status = await AssetInventory.status(forModules: modules) - while status == .downloading { - try await Task.sleep(nanoseconds: 100_000_000) - status = await AssetInventory.status(forModules: modules) - } - guard status == .installed else { - throw VoiceFailure("speech assets could not be installed for \(localeIdentifier)") - } - } - } - - private func startResults(from transcriber: DictationTranscriber) { - resultsTask = Task { [weak self] in - do { - for try await result in transcriber.results { - let text = String(result.text.characters) - await self?.emit( - kind: result.isFinal ? eventFinal : eventVolatile, - text: text - ) - } - } catch { - await self?.reportFailure(error) - } - } - } - - private func runFile(url: URL, analyzer: SpeechAnalyzer) async throws { - let file = try AVAudioFile(forReading: url) - try await analyzer.prepareToAnalyze(in: file.processingFormat) - emitReady() - let lastSample = try await analyzer.analyzeSequence(from: file) - if let lastSample { - try await analyzer.finalizeAndFinish(through: lastSample) - } else { - try await analyzer.finalizeAndFinishThroughEndOfInput() - } - await resultsTask?.value - emitTerminal(kind: eventEnded, text: nil) - } - - private func runMicrophone( - analyzer: SpeechAnalyzer, - modules: [any SpeechModule] - ) async throws { - let engine = AVAudioEngine() - let input = engine.inputNode - let naturalFormat = input.outputFormat(forBus: 0) - let authorization = AVCaptureDevice.authorizationStatus(for: .audio) - voiceLog( - "mic start: audio authorization=\(authorization.rawValue) " - + "(0 notDetermined, 1 restricted, 2 denied, 3 authorized), " - + "format=\(naturalFormat)" - ) - guard naturalFormat.sampleRate > 0, naturalFormat.channelCount > 0 else { - throw VoiceFailure("microphone has no usable audio format") - } - guard let analysisFormat = await SpeechAnalyzer.bestAvailableAudioFormat( - compatibleWith: modules, - considering: naturalFormat - ) else { - throw VoiceFailure("no compatible speech audio format") - } - - let feeder = AudioFeeder() - self.feeder = feeder - self.engine = engine - try await analyzer.prepareToAnalyze(in: analysisFormat) - analysisTask = Task { [weak self] in - do { - try await analyzer.start(inputSequence: feeder.stream) - } catch { - await self?.reportFailure(error) - } - } - - guard let converter = AVAudioConverter(from: naturalFormat, to: analysisFormat) else { - throw VoiceFailure("could not create microphone audio converter") - } - input.installTap(onBus: 0, bufferSize: 1_024, format: naturalFormat) { - buffer, _ in - if let level = Self.level(of: buffer) { - Task { [weak self] in - await self?.emitLevel(level) - } - } - do { - // A rate-converting AVAudioConverter may legitimately emit an - // empty buffer while it primes; skip those instead of failing. - if let converted = try Self.convert( - buffer, - with: converter, - to: analysisFormat - ) { - feeder.feed(converted) - } - } catch { - Task { [weak self] in - await self?.reportFailure(error) - } - } - } - engine.prepare() - try engine.start() - emitReady() - } - - /// Perceptual input level in 0...1: RMS mapped over a -50 dBFS..0 dBFS - /// range, which tracks speech dynamics far better than linear amplitude. - nonisolated private static func level(of buffer: AVAudioPCMBuffer) -> Float? { - guard let samples = buffer.floatChannelData?[0], buffer.frameLength > 0 else { - return nil - } - var sum: Float = 0 - for index in 0.. 0 else { return 0 } - let decibels = 20 * log10(rms) - return min(1, max(0, (decibels + 50) / 50)) - } - - nonisolated private static func convert( - _ input: AVAudioPCMBuffer, - with converter: AVAudioConverter, - to format: AVAudioFormat - ) throws -> AVAudioPCMBuffer? { - let ratio = format.sampleRate / input.format.sampleRate - let capacity = AVAudioFrameCount(ceil(Double(input.frameLength) * ratio)) + 1 - guard let output = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: capacity) else { - throw VoiceFailure("could not allocate converted microphone buffer") - } - - var suppliedInput = false - var conversionError: NSError? - let status = converter.convert(to: output, error: &conversionError) { _, state in - if suppliedInput { - state.pointee = .noDataNow - return nil - } - suppliedInput = true - state.pointee = .haveData - return input - } - if let conversionError { - throw conversionError - } - guard status == .haveData || status == .inputRanDry else { - throw VoiceFailure("microphone audio conversion failed") - } - return output.frameLength > 0 ? output : nil - } - - func stop() async { - guard !terminal, !stopping else { return } - stopping = true - stopCapture() - - do { - feeder?.finish() - if let analyzer { - try await analyzer.finalizeAndFinishThroughEndOfInput() - } - await analysisTask?.value - await resultsTask?.value - emitTerminal(kind: eventEnded, text: nil) - } catch { - emitTerminal(kind: eventError, text: error.localizedDescription) - } - } - - func cancel() async { - guard !terminal else { return } - stopping = true - stopCapture() - feeder?.finish() - if let analyzer { - await analyzer.cancelAndFinishNow() - } - analysisTask?.cancel() - resultsTask?.cancel() - emitTerminal(kind: eventEnded, text: nil) - } - - private func finishAsEnded(cancel: Bool) async { - if cancel, let analyzer { - await analyzer.cancelAndFinishNow() - } - emitTerminal(kind: eventEnded, text: nil) - } - - private func stopCapture() { - if let engine { - engine.inputNode.removeTap(onBus: 0) - engine.stop() - } - engine = nil - } - - private func emitReady() { - guard !terminal, !ready else { return } - ready = true - callback(context, eventReady, nil) - } - - private func reportFailure(_ error: Error) { - guard !stopping else { return } - emitTerminal(kind: eventError, text: error.localizedDescription) - } - - private func emitLevel(_ level: Float) { - guard !terminal, ready else { return } - String(format: "%.3f", level).withCString { callback(context, eventLevel, $0) } - } - - private func emit(kind: Int32, text: String) { - guard !terminal, ready else { - voiceLog("dropping event kind=\(kind) (terminal=\(terminal) ready=\(ready))") - return - } - text.withCString { callback(context, kind, $0) } - } - - private func emitTerminal(kind: Int32, text: String?) { - guard !terminal else { return } - terminal = true - voiceLog("terminal kind=\(kind) text=\(text ?? "nil")") - if let text { - text.withCString { callback(context, kind, $0) } - } else { - callback(context, kind, nil) - } - } -} - -/// Diagnostic line on stderr; shows up in the app log next to env_logger -/// output. Cheap and always on — dictation is short-lived and low-volume. -private func voiceLog(_ message: String) { - FileHandle.standardError.write(Data("[tcode-voice] \(message)\n".utf8)) -} - -private struct VoiceFailure: LocalizedError { - let message: String - - init(_ message: String) { - self.message = message - } - - var errorDescription: String? { message } -} - -private final class LockedFlag: @unchecked Sendable { - private let lock = NSLock() - private var value = false - - func set(_ newValue: Bool) { - lock.lock() - value = newValue - lock.unlock() - } - - func get() -> Bool { - lock.lock() - defer { lock.unlock() } - return value - } -} - -@available(macOS 26.0, *) -private final class VoiceSession: @unchecked Sendable { - let core: VoiceCore - - init( - locale: String, - source: VoiceSource, - context: UnsafeMutableRawPointer?, - callback: @escaping VoiceEventCallback - ) { - core = VoiceCore( - localeIdentifier: locale, - source: source, - context: context, - callback: callback - ) - Task { await core.run() } - } - - func stop() { - Task { await core.stop() } - } - - func cancel() { - Task { await core.cancel() } - } -} - -@_cdecl("voice_is_supported") -public func voiceIsSupported(_ localeCString: UnsafePointer?) -> Bool { - guard #available(macOS 26.0, *), let localeCString else { return false } - let identifier = String(cString: localeCString) - let semaphore = DispatchSemaphore(value: 0) - let supported = LockedFlag() - Task { - let result = await DictationTranscriber.supportedLocale( - equivalentTo: Locale(identifier: identifier) - ) != nil - supported.set(result) - semaphore.signal() - } - semaphore.wait() - return supported.get() -} - -@_cdecl("voice_start") -public func voiceStart( - _ localeCString: UnsafePointer?, - _ context: UnsafeMutableRawPointer?, - _ callback: VoiceEventCallback? -) -> UnsafeMutableRawPointer? { - guard #available(macOS 26.0, *), let localeCString, let callback else { return nil } - let session = VoiceSession( - locale: String(cString: localeCString), - source: .microphone, - context: context, - callback: callback - ) - return Unmanaged.passRetained(session).toOpaque() -} - -@_cdecl("voice_start_file") -public func voiceStartFile( - _ localeCString: UnsafePointer?, - _ pathCString: UnsafePointer?, - _ context: UnsafeMutableRawPointer?, - _ callback: VoiceEventCallback? -) -> UnsafeMutableRawPointer? { - guard #available(macOS 26.0, *), - let localeCString, - let pathCString, - let callback - else { return nil } - let session = VoiceSession( - locale: String(cString: localeCString), - source: .file(URL(fileURLWithPath: String(cString: pathCString))), - context: context, - callback: callback - ) - return Unmanaged.passRetained(session).toOpaque() -} - -@_cdecl("voice_stop") -public func voiceStop(_ pointer: UnsafeMutableRawPointer?) { - guard #available(macOS 26.0, *), let pointer else { return } - Unmanaged.fromOpaque(pointer).takeUnretainedValue().stop() -} - -@_cdecl("voice_free") -public func voiceFree(_ pointer: UnsafeMutableRawPointer?) { - guard #available(macOS 26.0, *), let pointer else { return } - let unmanaged = Unmanaged.fromOpaque(pointer) - unmanaged.takeUnretainedValue().cancel() - unmanaged.release() -} diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 50735f9f7..6d83aad89 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -12,11 +12,11 @@ default = [] debug-exports = [] [target.'cfg(target_family = "wasm")'.dependencies] -gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false } # The browser constructs its own platform (gpui::Application::with_platform) # rather than going through gpui-pre-platform, whose wasm arm enables # gpui-pre-web's nightly-only "multithreaded" default. -gpui_web = { package = "gpui-pre-web", version = "=0.3.7", default-features = false } +gpui_web = { package = "gpui-pre-web", version = "=0.3.8", default-features = false } console_error_panic_hook = "0.1" tcode-protocol = { path = "../protocol", default-features = false } tcode-client = { path = "../client" } diff --git a/locales/en.yml b/locales/en.yml index 40b20c107..3a69c23c6 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -712,6 +712,7 @@ composer: model_results: "Models" model_option: "Model %{model}" voice_start: "Dictate a message" + voice_unavailable: "Dictation is unavailable" voice_preparing: "Preparing dictation…" voice_stop: "Stop dictation" voice_error: "Dictation failed: %{error}" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index b6828f15c..693dd22ea 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -706,6 +706,7 @@ composer: model_results: "模型" model_option: "模型 %{model}" voice_start: "语音输入" + voice_unavailable: "语音输入不可用" voice_preparing: "正在准备语音输入…" voice_stop: "停止语音输入" voice_error: "语音输入失败:%{error}"