From b115cec096000f60288f2701d3c9501d3549da95 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Thu, 9 Apr 2026 20:31:07 -0400 Subject: [PATCH] Run UI on a dedicated OS thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI event loop was running on the same tokio runtime as inference, tool execution, and background agents. When the runtime was busy, the UI's select loop couldn't wake up to render — causing visible latency and input lag. Give the UI its own OS thread with a dedicated single-threaded tokio runtime. The mind loop stays on the main runtime. Cross-runtime communication (channels, watch, Notify) works unchanged. Also drops the tokio-scoped dependency, which was only used to scope the two tasks together. Co-Authored-By: Proof of Concept --- Cargo.lock | 22 ---------------------- Cargo.toml | 1 - src/user/mod.rs | 39 ++++++++++++++++++++++----------------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 882f939..a2c0262 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,7 +559,6 @@ dependencies = [ "tokenizers", "tokio", "tokio-rustls", - "tokio-scoped", "tokio-util", "tui-markdown", "tui-textarea-2", @@ -3160,27 +3159,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-scoped" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4beb8ba13bc53ac53ce1d52b42f02e5d8060f0f42138862869beb769722b256" -dependencies = [ - "tokio", - "tokio-stream", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.18" diff --git a/Cargo.toml b/Cargo.toml index 096c390..c97840f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,6 @@ rayon = "1" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["compat"] } -tokio-scoped = "0.2.0" futures = "0.3" capnp = "0.25" capnp-rpc = "0.25" diff --git a/src/user/mod.rs b/src/user/mod.rs index 9ec1de6..8904dcd 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -190,25 +190,30 @@ async fn start(cli: crate::user::CliArgs) -> Result<()> { let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1); let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel(); - let mind = crate::mind::Mind::new(config, turn_tx).await; + let mind = std::sync::Arc::new(crate::mind::Mind::new(config, turn_tx).await); - let mut result = Ok(()); - tokio_scoped::scope(|s| { - // Mind event loop — init + run - s.spawn(async { - mind.init().await; - mind.run(mind_rx, turn_rx).await; - }); + // UI runs on a dedicated OS thread so CPU-intensive work on the + // main tokio runtime can't starve rendering. + let ui_mind = mind.clone(); + let ui_handle = std::thread::Builder::new() + .name("ui".into()) + .spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("UI tokio runtime"); + rt.block_on(run( + tui::App::new(String::new(), ui_mind.agent.clone()), + &ui_mind, mind_tx, + )) + }) + .expect("spawn UI thread"); - // UI event loop - s.spawn(async { - result = run( - tui::App::new(String::new(), mind.agent.clone()), - &mind, mind_tx, - ).await; - }); - }); - result + // Mind event loop — runs on the main tokio runtime + mind.init().await; + mind.run(mind_rx, turn_rx).await; + + ui_handle.join().unwrap_or_else(|_| Err(anyhow::anyhow!("UI thread panicked"))) } fn hotkey_cycle_reasoning(mind: &crate::mind::Mind) {