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