From 52eadf3be2a779fa1471f947b1507b86a9c1ae2c Mon Sep 17 00:00:00 2001 From: Tryanks Date: Tue, 6 Oct 2026 16:01:32 +0800 Subject: [PATCH 1/2] build(deps): GPUI Kit 0.7.1 on gpui-fast v0.1.0; composer dictation via gpui-component speech Upgrade gpui-base, gpui-kit-assets, gpui-component-macros and gpui-wry to 0.7.1, the gpui-pre family to 0.3.8 and the gpui-fast patch to 3755a552 (v0.1.0, zed a1b7107). Three call sites follow upstream API changes: wrap_line's indent argument, RequestFrameOptions' signal fields and the optional WgpuRenderer gpu_specs on iOS and Android. Replace the macOS-26-only SpeechAnalyzer shim (crates/voice) with gpui-component's speech module: SpeechState with the system recognizer (SFSpeechRecognizer on macOS 13+, Windows.Media.SpeechRecognition on Windows; none on Linux, so the mic button is absent there) and the cpal microphone. The composer keeps its own styled mic button over the kit state and replaces the session's whole transcript at an anchor; user edits, submit, destination switches and teardown cancel in place. The desktop binary embeds the usage descriptions so cargo run can ask for permission. --- CONTRIBUTING.md | 1 - Cargo.lock | 896 ++++++++++-------- Cargo.toml | 1 - crates/android/Cargo.toml | 2 +- crates/app/Cargo.toml | 4 +- crates/app/Info.plist | 10 + crates/app/build.rs | 9 + crates/ios/Cargo.toml | 2 +- crates/platform/gpui-android/Cargo.toml | 2 +- .../gpui-android/src/android/window.rs | 17 +- crates/platform/gpui-ios/Cargo.toml | 2 +- crates/platform/gpui-ios/src/ios/window.rs | 20 +- crates/ui/Cargo.toml | 22 +- crates/ui/src/composer/components/mod.rs | 4 +- crates/ui/src/composer/components/voice.rs | 524 +++++----- crates/ui/src/composer/mod.rs | 29 +- crates/ui/src/markdown/inline_flow.rs | 2 +- crates/voice/Cargo.toml | 7 - crates/voice/build.rs | 113 --- crates/voice/examples/file_dictation.rs | 42 - crates/voice/examples/mic_dictation.rs | 19 - crates/voice/src/imp.rs | 159 ---- crates/voice/src/lib.rs | 113 --- crates/voice/swift/shim.swift | 498 ---------- crates/web/Cargo.toml | 4 +- locales/en.yml | 1 + locales/zh-CN.yml | 1 + 27 files changed, 888 insertions(+), 1616 deletions(-) create mode 100644 crates/app/Info.plist delete mode 100644 crates/voice/Cargo.toml delete mode 100644 crates/voice/build.rs delete mode 100644 crates/voice/examples/file_dictation.rs delete mode 100644 crates/voice/examples/mic_dictation.rs delete mode 100644 crates/voice/src/imp.rs delete mode 100644 crates/voice/src/lib.rs delete mode 100644 crates/voice/swift/shim.swift 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 7664f492b..38622c852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,28 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[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" @@ -276,9 +298,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", ] @@ -540,18 +562,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" @@ -975,8 +985,6 @@ dependencies = [ "cexpr", "clang-sys", "itertools 0.13.0", - "log", - "prettyplease", "proc-macro2", "quote", "regex", @@ -1122,16 +1130,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 = "bs58" version = "0.5.1" @@ -1201,15 +1199,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" @@ -1422,9 +1411,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", @@ -1569,7 +1558,7 @@ dependencies = [ [[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", @@ -1598,24 +1587,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" @@ -1893,6 +1864,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", +] + [[package]] name = "cosmic-text" version = "0.19.0" @@ -1917,6 +1908,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" @@ -2220,6 +2234,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" @@ -2243,7 +2263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 3.0.5", + "syn 1.0.109", ] [[package]] @@ -2367,7 +2387,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", @@ -2657,6 +2677,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" @@ -2748,17 +2788,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" @@ -2842,9 +2871,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" @@ -2966,7 +2992,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", @@ -3075,6 +3100,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 = "futf" version = "0.1.5" @@ -3403,6 +3437,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" @@ -3620,14 +3664,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", "bitflags 2.13.2", "chrono", "collections", @@ -3647,6 +3690,7 @@ dependencies = [ "image", "inventory", "itertools 0.14.0", + "libc", "log", "lyon", "num_cpus", @@ -3661,7 +3705,7 @@ dependencies = [ "raw-window-handle", "refineable", "regex", - "resvg", + "resvg 0.46.0", "scheduler", "schemars 1.2.2", "seahash", @@ -3674,18 +3718,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]] @@ -3699,7 +3739,7 @@ dependencies = [ "gpui_wgpu", "jni 0.22.4", "log", - "ndk", + "ndk 0.9.0", "parking_lot", "raw-window-handle", "smallvec", @@ -3709,9 +3749,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", @@ -3742,16 +3782,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", @@ -3782,9 +3872,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", @@ -3797,24 +3887,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", @@ -3872,33 +3962,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", @@ -3908,38 +3998,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", @@ -3968,7 +4063,6 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "wayland-protocols-wlr", - "x11-clipboard", "x11rb", "xkbcommon", "zed-scap", @@ -3978,13 +4072,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", @@ -3992,6 +4084,7 @@ dependencies = [ "core-foundation-sys", "core-graphics 0.24.0", "core-text", + "core-video", "ctor", "dispatch2", "foreign-types", @@ -4001,20 +4094,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", @@ -4025,7 +4114,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", @@ -4036,8 +4125,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", @@ -4049,7 +4139,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", @@ -4059,7 +4149,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", @@ -4069,7 +4159,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", @@ -4097,7 +4187,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", @@ -4123,7 +4213,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", @@ -4140,6 +4230,7 @@ dependencies = [ "parking_lot", "rand 0.9.4", "raw-window-handle", + "scheduler", "smallvec", "uuid", "windows 0.62.2", @@ -4435,12 +4526,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", @@ -4455,7 +4544,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", @@ -4742,7 +4831,7 @@ dependencies = [ "byteorder-lite", "color_quant", "exr", - "gif", + "gif 0.14.2", "image-webp", "moxcms", "num-traits", @@ -4751,8 +4840,8 @@ dependencies = [ "ravif", "rayon", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.1", + "zune-jpeg 0.5.15", ] [[package]] @@ -4765,6 +4854,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" @@ -4810,6 +4905,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" @@ -5299,6 +5414,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" @@ -5311,6 +5446,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" @@ -5351,7 +5497,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", @@ -5396,12 +5542,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" @@ -5495,7 +5635,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "serde_core", - "value-bag", ] [[package]] @@ -5628,13 +5767,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" @@ -5773,20 +5918,6 @@ dependencies = [ "socket2", ] -[[package]] -name = "media" -version = "0.1.0" -source = "git+https://github.com/longbridge/gpui-fast#1b381adb68a6e5a4550f73b0cd29a231ce6c7281" -dependencies = [ - "anyhow", - "bindgen", - "core-foundation 0.10.1", - "core-video", - "foreign-types", - "metal", - "objc", -] - [[package]] name = "memchr" version = "2.8.3" @@ -6038,6 +6169,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" @@ -6047,7 +6192,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", @@ -6059,6 +6204,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" @@ -6372,6 +6526,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" @@ -6386,6 +6558,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" @@ -6538,7 +6719,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.118", @@ -6628,6 +6809,27 @@ dependencies = [ "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", +] + [[package]] name = "objc2-cloud-kit" version = "0.3.2" @@ -6710,16 +6912,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" @@ -6883,35 +7075,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", ] @@ -6950,7 +7142,6 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "objc2 0.6.4", - "objc2-core-location", "objc2-foundation 0.3.2", ] @@ -6997,6 +7188,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" @@ -7286,16 +7500,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" @@ -7728,8 +7932,6 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures", - "log", "parking_lot", "pin-project", "pollster 0.2.5", @@ -7797,16 +7999,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.118", -] - [[package]] name = "preview-mcp" version = "0.1.0" @@ -8446,7 +8638,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", ] @@ -8545,7 +8737,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", @@ -8559,21 +8751,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]] @@ -9052,7 +9261,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", @@ -9282,15 +9491,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" @@ -9490,7 +9690,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]] @@ -9659,7 +9859,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" dependencies = [ - "borsh", "serde_core", ] @@ -9865,106 +10064,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", ] @@ -10424,6 +10554,7 @@ dependencies = [ "futures-lite", "gpui-android", "gpui-base", + "gpui-component", "gpui-component-macros", "gpui-kit-assets", "gpui-pre", @@ -10458,7 +10589,6 @@ dependencies = [ "tcode-runtime", "tcode-services", "tcode-traverse", - "tcode-voice", "tempfile", "unicode-segmentation", "unicode-width", @@ -10469,10 +10599,6 @@ dependencies = [ "zed-reqwest", ] -[[package]] -name = "tcode-voice" -version = "0.1.0" - [[package]] name = "tcode-web" version = "0.1.0" @@ -10612,7 +10738,7 @@ dependencies = [ "half", "quick-error 2.0.1", "weezl", - "zune-jpeg", + "zune-jpeg 0.5.15", ] [[package]] @@ -11119,12 +11245,6 @@ dependencies = [ "thiserror 2.0.19", ] -[[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" @@ -11301,6 +11421,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" @@ -11311,8 +11458,8 @@ dependencies = [ "data-url", "flate2", "fontdb", - "imagesize", - "kurbo", + "imagesize 0.14.0", + "kurbo 0.13.1", "log", "pico-args", "roxmltree 0.21.1", @@ -11320,7 +11467,7 @@ dependencies = [ "simplecss", "siphasher 1.0.3", "strict-num", - "svgtypes", + "svgtypes 0.16.1", "tiny-skia-path", "unicode-bidi", "unicode-script", @@ -11346,16 +11493,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" @@ -11386,42 +11523,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" @@ -11959,7 +12060,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", @@ -12057,6 +12158,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.57.0" @@ -12133,6 +12244,16 @@ dependencies = [ "windows-core 0.62.2", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -12450,6 +12571,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" @@ -12840,16 +12970,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" @@ -13202,7 +13322,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", @@ -13216,7 +13335,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -13401,17 +13519,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" @@ -13421,18 +13528,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" @@ -13449,13 +13560,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 2ee40f8aa..bdde6149a 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::*; @@ -202,7 +202,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, } @@ -347,7 +347,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(); @@ -410,6 +410,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, @@ -445,8 +452,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, } } @@ -466,7 +473,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| { @@ -586,6 +593,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. @@ -728,7 +737,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)); @@ -1074,13 +1083,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 @@ -1300,7 +1309,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 207f81e60..fc2aa0b17 100644 --- a/crates/ui/src/markdown/inline_flow.rs +++ b/crates/ui/src/markdown/inline_flow.rs @@ -661,7 +661,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 788ee4b12..2cda557d2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -701,6 +701,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 35cda3304..abd8d3a39 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -695,6 +695,7 @@ composer: model_results: "模型" model_option: "模型 %{model}" voice_start: "语音输入" + voice_unavailable: "语音输入不可用" voice_preparing: "正在准备语音输入…" voice_stop: "停止语音输入" voice_error: "语音输入失败:%{error}" From fa9d0824bb449d9384a31d6ba562442d44e79f0f Mon Sep 17 00:00:00 2001 From: Tryanks Date: Tue, 6 Oct 2026 16:22:48 +0800 Subject: [PATCH 2/2] build(deps): keep gpu-allocator on windows 0.62 for wgpu-hal's D3D12 types gpui-base 0.7.1 pulls windows 0.58, which gpu-allocator 0.28 also accepts, so the resolver unified it there; wgpu-hal 29 needs the 0.62 types (#555). --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 80d30ad6d..00ea24def 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3807,7 +3807,7 @@ dependencies = [ "log", "presser", "thiserror 2.0.19", - "windows 0.58.0", + "windows 0.62.2", ] [[package]]