Run UI on a dedicated OS thread

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 <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-09 20:31:07 -04:00
parent d3f0b3f3f7
commit b115cec096
3 changed files with 22 additions and 40 deletions

22
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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) {