@setoelkahfi / sigit / commits / 9548958

Switch to local onde dependency and update model handling

- Use local onde crate via path dependency - Update crossterm and related dependencies in Cargo.lock - Switch mistralrs crates to git source - Add model selection and listing to chat TUI - Improve model config and sampling handling for Qwen 3 - Update comments and clarify code in chat and main modules

Seto Elkahfi committed Apr 14, 2026 at 19:20 UTC 9548958673868927782b43c0e7137c4c6b09402c
4 files changed +208 -102
Cargo.lock
+40 -56
index 2c24312..a994f4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1056,15 +1056,16 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.25.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.0", "crossterm_winapi", - "libc", - "mio 0.8.11", + "futures-core", + "mio", "parking_lot", + "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", @@ -1072,16 +1073,17 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.11.0", "crossterm_winapi", - "futures-core", - "mio 1.2.0", + "derive_more", + "document-features", + "mio", "parking_lot", - "rustix 0.38.44", + "rustix 1.1.4", "signal-hook", "signal-hook-mio", "winapi", @@ -1435,6 +1437,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359" +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dtoa" version = "1.0.11" @@ -3016,6 +3027,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "llguidance" version = "1.7.2" @@ -3270,18 +3287,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.2.0" @@ -3297,8 +3302,7 @@ dependencies = [ [[package]] name = "mistralrs" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb0a83340b4492ebba9760ba6845364de369c7e10c1f91af8733d574c7405fa" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "anyhow", "candle-core", @@ -3325,8 +3329,7 @@ dependencies = [ [[package]] name = "mistralrs-audio" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5d36f634c7c20c45845bc995be3b6387ab01048410386a5b180cfeaf89c72" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "anyhow", "apodize", @@ -3337,8 +3340,7 @@ dependencies = [ [[package]] name = "mistralrs-core" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b8b9e5c94491d9ceeded3a30291cb6af6ae74810a5776dd55e3bbb8b3429d4" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "ahash", "akin", @@ -3435,8 +3437,7 @@ dependencies = [ [[package]] name = "mistralrs-macros" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa9b4794322d3f89fe61d21f33f67be494c16d5bd869604b111218f32544d32" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -3447,8 +3448,7 @@ dependencies = [ [[package]] name = "mistralrs-mcp" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa97d4e3189ed80ebbc730b7da5b2356df0ae004ab489066249340c33c089ab" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "anyhow", "async-trait", @@ -3468,8 +3468,7 @@ dependencies = [ [[package]] name = "mistralrs-paged-attn" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e53ddf1537426997b46abdadbe6ca8a7ce7668004ed2b7cf00115016558bae8" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "anyhow", "candle-core", @@ -3485,8 +3484,7 @@ dependencies = [ [[package]] name = "mistralrs-quant" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980715493d252e9aaf0779c4c101576d67ef4dd9812bfd77a54987f6f17526f4" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "byteorder", "candle-core", @@ -3515,8 +3513,7 @@ dependencies = [ [[package]] name = "mistralrs-vision" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10046a3da2de5b702d3e829b5ddef7aa829ad454819dcc4876dea481a6cb5456" +source = "git+https://github.com/setoelkahfi/mistral.rs?branch=fix%2Fall-platform-fixes#a27af8ea01123e5d5777120619413345989f4006" dependencies = [ "candle-core", "image", @@ -3830,9 +3827,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onde" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5436633863ffc31311f529439353ba06a436dd661d2a5d93c8793c3500123a13" +version = "0.1.3" dependencies = [ "anyhow", "cc", @@ -5330,8 +5325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 0.8.11", - "mio 1.2.0", + "mio", "signal-hook", ] @@ -6112,7 +6106,7 @@ checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", - "mio 1.2.0", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -6292,11 +6286,10 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tqdm" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b316d5c2ac649ca856dacd487d0ebb94f3b746bada51355d93dd2c007ab62a2e" +source = "git+https://github.com/setoelkahfi/tqdm?branch=deps%2Fbump-crossterm#41e4182829136e6dadbdd1c36af824d19219147a" dependencies = [ "anyhow", - "crossterm 0.25.0", + "crossterm 0.29.0", "once_cell", ] @@ -7273,15 +7266,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0"
Cargo.toml
+1 -1
index 550dc65..a5d8bca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" agent-client-protocol = "0.10.4" # Onde Inference engine (local LLM) -onde = "0.1.1" +onde = { path = "../onde" } # Async runtime async-trait = "0.1"
src/chat.rs
+144 -26
index c85a611..6015630 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,14 +1,13 @@ -//! Full-screen terminal chat UI for siGit Code. -//! -//! Takes over the alternate screen and multiplexes terminal events with -//! streaming LLM tokens via `tokio::select!`. +//! Full-screen chat TUI. Runs on the alternate screen so nothing leaks into +//! the main terminal buffer. Streaming tokens and key events share the loop +//! via `tokio::select!`. use std::future::pending; use anyhow::Result; use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use futures::StreamExt; -use onde::inference::{ChatEngine, StreamChunk}; +use onde::inference::{ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk}; use ratatui::{ Frame, layout::{Constraint, Layout, Position}, @@ -25,7 +24,7 @@ enum Role { User, Assistant, System, - /// Banner art — rendered with per-character digit colorization. + /// Banner art — each character gets its own color. Banner, } @@ -74,7 +73,7 @@ struct App { stream_rx: Option<mpsc::Receiver<StreamChunk>>, stream_buf: String, quit: bool, - /// Toggled every other tick while streaming — drives the blinking cursor. + /// Flips every few ticks while streaming to make the cursor blink. blink_on: bool, blink_counter: u8, } @@ -143,17 +142,17 @@ impl App { self.blink_on = self.blink_counter % 4 < 2; } - /// Total lines the messages area would need (rough estimate for scrolling). + /// Rough line count for the messages area — used to drive auto-scroll. fn total_message_lines(&self, width: u16) -> u16 { if width == 0 { return 0; } - let w = width.saturating_sub(2) as usize; // account for block borders + let w = width.saturating_sub(2) as usize; // subtract border columns let mut lines: u16 = 0; for msg in &self.messages { lines += wrapped_line_count(&msg.text, msg.role, w); } - // streaming buffer + // count any in-progress streaming text too if !self.stream_buf.is_empty() { lines += wrapped_line_count(&self.stream_buf, Role::Assistant, w); } @@ -174,7 +173,7 @@ fn banner_char_color(_ch: char) -> Color { Color::White } -/// Estimate how many terminal rows a message takes once wrapped. +/// How many terminal rows a message takes up after line-wrapping. fn wrapped_line_count(text: &str, role: Role, width: usize) -> u16 { let prefix_len = match role { Role::User => 6, // "you > " @@ -198,12 +197,52 @@ fn wrapped_line_count(text: &str, role: Role, width: usize) -> u16 { count.max(1) } +// ── Model table ────────────────────────────────────────────────────────────── + +struct ModelOption { + /// Name shown in `/models`. Must match `GgufModelConfig::display_name`. + name: &'static str, + /// Short blurb shown next to the name, e.g. "~2.7 GB". + description: &'static str, + /// True if this model actually handles tool calls. + tool_calling: bool, + /// Token budget for generation. Qwen 3 needs 4096+ or it outputs nothing. + max_tokens: u64, + config_fn: fn() -> GgufModelConfig, +} + +const SIGIT_MODELS: &[ModelOption] = &[ + ModelOption { + name: "Qwen 3 4B (Q4_K_M)", + description: "~2.7 GB", + tool_calling: true, + max_tokens: 4096, + config_fn: GgufModelConfig::qwen3_4b, + }, + ModelOption { + name: "Qwen 2.5 Coder 3B (Q4_K_M)", + description: "~1.93 GB", + tool_calling: false, + max_tokens: 512, + config_fn: GgufModelConfig::qwen25_coder_3b, + }, + ModelOption { + name: "Qwen 2.5 Coder 1.5B (Q4_K_M)", + description: "~941 MB", + tool_calling: false, + max_tokens: 512, + config_fn: GgufModelConfig::qwen25_coder_1_5b, + }, +]; + // ── Slash commands ─────────────────────────────────────────────────────────── enum SlashCommand { Help, Clear, Status, + /// `/models` lists models. `/models N` switches to model N (1-based). + Models(Option<usize>), Exit, Unknown(String), } @@ -213,11 +252,14 @@ fn parse_slash(input: &str) -> Option<SlashCommand> { if !trimmed.starts_with('/') { return None; } - let cmd = trimmed.split_whitespace().next().unwrap_or(""); + let mut parts = trimmed.splitn(2, char::is_whitespace); + let cmd = parts.next().unwrap_or(""); + let arg = parts.next().map(|s| s.trim()); Some(match cmd { "/help" => SlashCommand::Help, "/clear" => SlashCommand::Clear, "/status" => SlashCommand::Status, + "/models" => SlashCommand::Models(arg.and_then(|s| s.parse::<usize>().ok())), "/exit" | "/quit" | "/q" => SlashCommand::Exit, other => SlashCommand::Unknown(other.to_string()), }) @@ -283,7 +325,7 @@ fn render_messages(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect let buf_lines: Vec<&str> = app.stream_buf.split('\n').collect(); for (i, segment) in buf_lines.iter().enumerate() { if i > 0 { - lines.push(Line::from(spans.drain(..).collect::<Vec<_>>())); + lines.push(Line::from(std::mem::take(&mut spans))); // continuation lines get no prefix } spans.push(Span::raw(segment.to_string())); @@ -500,14 +542,21 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<String> { // ── Slash command execution ────────────────────────────────────────────────── -async fn exec_slash(app: &mut App, cmd: SlashCommand, engine: &ChatEngine) { +async fn exec_slash( + app: &mut App, + cmd: SlashCommand, + engine: &ChatEngine, + terminal: &mut ratatui::DefaultTerminal, +) { match cmd { SlashCommand::Help => { app.messages.push(ChatMessage::system( - "/help — show this message\n\ - /clear — wipe conversation history\n\ - /status — show engine status\n\ - /exit — quit chat", + "/help — show this message\n\ + /models — list available models\n\ + /models N — switch to model N\n\ + /clear — wipe conversation history\n\ + /status — show engine status\n\ + /exit — quit chat", )); } SlashCommand::Clear => { @@ -527,6 +576,77 @@ async fn exec_slash(app: &mut App, cmd: SlashCommand, engine: &ChatEngine) { info.status, model, mem, info.history_length, ))); } + SlashCommand::Models(selection) => match selection { + None => { + // Show the model list. + let info = engine.info().await; + let current = info.model_name.clone().unwrap_or_default(); + + let mut text = String::from("Available models — type /models <n> to switch:\n"); + for (i, model) in SIGIT_MODELS.iter().enumerate() { + let current_marker = if current == model.name { + " ← current" + } else { + "" + }; + let tool_badge = if model.tool_calling { + " ✓ tool calling" + } else { + "" + }; + text.push_str(&format!( + "\n {} {} {}{}{}", + i + 1, + model.name, + model.description, + tool_badge, + current_marker, + )); + } + app.messages.push(ChatMessage::system(text)); + } + Some(n) => { + let idx = n.saturating_sub(1); + match SIGIT_MODELS.get(idx) { + None => { + app.messages.push(ChatMessage::system(format!( + "error: no model #{n} — type /models to see the list." + ))); + } + Some(model) => { + // Redraw first — "Loading…" has to be on screen before + // we block for however long the load takes. + app.messages + .push(ChatMessage::system(format!("Loading {}…", model.name))); + terminal.draw(|frame| render(frame, app)).ok(); + + engine.unload_model().await; + + let config = (model.config_fn)(); + let sampling = SamplingConfig { + max_tokens: Some(model.max_tokens), + ..SamplingConfig::default() + }; + + match engine.load_gguf_model(config, None, Some(sampling)).await { + Ok(_) => { + engine.clear_history().await; + app.messages.push(ChatMessage::system(format!( + "✓ Switched to {}", + model.name + ))); + } + Err(err) => { + app.messages.push(ChatMessage::system(format!( + "error loading {}: {err}", + model.name + ))); + } + } + } + } + } + }, SlashCommand::Exit => { app.quit = true; } @@ -539,9 +659,7 @@ async fn exec_slash(app: &mut App, cmd: SlashCommand, engine: &ChatEngine) { // ── Main loop ──────────────────────────────────────────────────────────────── -/// Run the interactive chat UI. Blocks until the user quits. -/// -/// The caller must have already loaded a model into `engine`. +/// Starts the TUI and blocks until the user quits. Model must be loaded first. pub async fn run(engine: &ChatEngine) -> Result<()> { let mut terminal = ratatui::init(); let result = event_loop(&mut terminal, engine).await; @@ -554,7 +672,7 @@ async fn event_loop(terminal: &mut ratatui::DefaultTerminal, engine: &ChatEngine let mut event_stream = EventStream::new(); loop { - // draw + // redraw every iteration terminal.draw(|frame| render(frame, &mut app))?; if app.quit { @@ -565,7 +683,7 @@ async fn event_loop(terminal: &mut ratatui::DefaultTerminal, engine: &ChatEngine tokio::select! { biased; - // streaming chunks — only active when we have a receiver + // streaming tokens — only polls when there's an active receiver chunk = async { match app.stream_rx.as_mut() { Some(rx) => rx.recv().await, @@ -596,12 +714,12 @@ async fn event_loop(terminal: &mut ratatui::DefaultTerminal, engine: &ChatEngine }; if let Event::Key(key) = event { - // while streaming, only ctrl+c/d work + // ignore everything except Ctrl+C while a response is coming in if app.is_streaming() { if key.kind == KeyEventKind::Press { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); if ctrl && (key.code == KeyCode::Char('c') || key.code == KeyCode::Char('d')) { - // drop the receiver to stop reading + // dropping the receiver makes the stream drain itself app.finalize_stream(); app.messages.push(ChatMessage::system("(cancelled)")); } @@ -612,7 +730,7 @@ async fn event_loop(terminal: &mut ratatui::DefaultTerminal, engine: &ChatEngine if let Some(text) = handle_key(&mut app, key) { // check for slash command first if let Some(cmd) = parse_slash(&text) { - exec_slash(&mut app, cmd, engine).await; + exec_slash(&mut app, cmd, engine, terminal).await; continue; }
src/main.rs
+23 -19
index 346a17f..66c8d79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ -//! siGit Code — AI coding agent powered by a local LLM via Onde Inference. +//! siGit Code — local LLM coding agent built on Onde. //! -//! Two modes of operation: +//! Two modes: //! -//! - **Interactive** (stdin is a TTY): full-screen chat UI built on ratatui. -//! - **ACP server** (stdin is piped): JSON-RPC over stdio for editors like Zed. +//! - Interactive (stdin is a TTY): full-screen TUI via ratatui. +//! - ACP server (stdin is piped): JSON-RPC over stdio for Zed and friends. //! //! On macOS the model cache is shared with the siGit desktop app through an //! App Group container. See [`setup`]. @@ -58,8 +58,7 @@ root cause, not the symptom. Correct beats clever."; // ── Per-session state ──────────────────────────────────────────────────────── -/// One active session at a time. We store the `SessionId` directly (not as a -/// `String`) so `==` just works. +/// One active session. `SessionId` stored directly (not `String`) so `==` works. struct Session { id: SessionId, } @@ -70,7 +69,7 @@ struct Session { struct SiGitAgent { engine: Arc<ChatEngine>, active_session: Arc<Mutex<Option<Session>>>, - /// Sends streaming chunks to the forwarder task, which writes them out. + /// Streaming chunks go here; a separate task drains this and writes to the editor. notification_tx: mpsc::Sender<SessionNotification>, } @@ -116,7 +115,7 @@ impl Agent for SiGitAgent { log::info!("new_session: id={session_id}"); if self.engine.is_loaded().await { - // Model is already warm — just wipe the conversation. + // Already loaded — just clear history for the new session. log::info!("model already loaded — clearing history for new session"); self.engine.clear_history().await; } else { @@ -215,14 +214,20 @@ impl Agent for SiGitAgent { // ── Interactive mode ───────────────────────────────────────────────────────── -/// Load the model, then hand off to the ratatui chat TUI. +/// Loads the model and starts the TUI. async fn run_interactive() -> anyhow::Result<()> { println!(" Loading siGit..."); let engine = ChatEngine::new(); - let config = GgufModelConfig::platform_default(); + let config = GgufModelConfig::qwen3_4b(); + // Qwen 3's <think>…</think> block burns 300-400 tokens before the actual reply. + // The default 512 isn't enough — the model outputs nothing. 4096 fixes it. + let sampling = onde::inference::SamplingConfig { + max_tokens: Some(4096), + ..onde::inference::SamplingConfig::default() + }; engine - .load_gguf_model(config, Some(SYSTEM_PROMPT.to_string()), None) + .load_gguf_model(config, Some(SYSTEM_PROMPT.to_string()), Some(sampling)) .await .map_err(|e| anyhow::anyhow!("model load failed: {e}"))?; @@ -238,9 +243,9 @@ async fn run_interactive() -> anyhow::Result<()> { // ── ACP server mode ────────────────────────────────────────────────────────── -/// The editor spawns us and talks ACP over stdio. +/// The editor spawns us as a subprocess and speaks ACP over stdio. async fn run_acp_server() -> anyhow::Result<()> { - // Agent::prompt sends chunks here; the forwarder task writes them out. + // Chunks from Agent::prompt land here; a forwarder task ships them to the editor. let (notification_tx, mut notification_rx) = mpsc::channel::<SessionNotification>(256); let agent = SiGitAgent::new(notification_tx); @@ -254,7 +259,7 @@ async fn run_acp_server() -> anyhow::Result<()> { local .run_until(async move { - // Wire up the ACP connection. + // Connect stdin/stdout to the ACP layer. let (conn, io_task) = AgentSideConnection::new( agent, stdout, @@ -264,7 +269,7 @@ async fn run_acp_server() -> anyhow::Result<()> { }, ); - // Forwarder: drains the mpsc channel and pushes chunks to the client. + // Pull chunks off the channel and push them to the editor as they arrive. tokio::task::spawn_local(async move { while let Some(notification) = notification_rx.recv().await { if let Err(err) = conn.session_notification(notification).await { @@ -273,7 +278,7 @@ async fn run_acp_server() -> anyhow::Result<()> { } }); - // Blocks until the editor disconnects. + // Runs until the editor closes the connection. if let Err(err) = io_task.await { log::error!("ACP IO error: {err}"); } @@ -287,13 +292,12 @@ async fn run_acp_server() -> anyhow::Result<()> { #[tokio::main] async fn main() -> anyhow::Result<()> { - // Logs always go to stderr (stdout is either the TUI or the ACP wire). + // stderr for logs — stdout belongs to either the TUI or the ACP wire. env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .target(env_logger::Target::Stderr) .init(); - // Shared model cache (macOS App Group) — must run before anything - // touches hf-hub or ChatEngine. + // Set up the shared model cache before anything tries to hit hf-hub. setup::setup_shared_model_cache(); if std::io::stdin().is_terminal() {