From eba7d89006f0a3ff050a414baffb833ab778a9aa Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Thu, 9 Apr 2026 09:35:55 +0100 Subject: [PATCH] use sendMessageDraft for native streaming output, fallback to editMessageText Telegram Bot API 9.3+ sendMessageDraft provides smooth streaming text rendering without the flickering of repeated edits. Falls back to editMessageText automatically if the API is unavailable (e.g. older clients or group chats). Also reduces edit interval from 5s to 3s and uses 1s interval for draft mode. --- Cargo.lock | 397 +++++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + src/main.rs | 164 +++++++++++++++------- 3 files changed, 492 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19280b8..1ec623a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -51,6 +57,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -382,6 +394,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -406,7 +429,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -452,6 +494,16 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -459,7 +511,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -485,9 +560,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -499,6 +574,43 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.9.0", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -506,12 +618,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration 0.7.0", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -669,6 +822,16 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.9.0" @@ -813,6 +976,7 @@ dependencies = [ "anyhow", "chrono", "dptree", + "reqwest 0.12.28", "serde", "serde_json", "serde_yaml", @@ -1060,16 +1224,16 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -1083,8 +1247,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-util", @@ -1097,6 +1261,60 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.9.0", + "hyper-rustls", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1119,13 +1337,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1339,6 +1590,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1367,6 +1624,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1386,7 +1652,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -1399,6 +1676,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -1455,7 +1742,7 @@ dependencies = [ "once_cell", "pin-project", "rc-box", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "serde_with_macros", @@ -1487,7 +1774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1570,6 +1857,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1594,6 +1891,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -1691,6 +2027,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -1716,7 +2058,7 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom", + "getrandom 0.4.2", "js-sys", "sha1_smol", "wasm-bindgen", @@ -1926,6 +2268,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2240,6 +2593,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index dbeee0f..8fe2a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ dptree = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +reqwest = { version = "0.12", features = ["json"] } teloxide = { version = "0.12", features = ["macros"] } tokio = { version = "1", features = ["full"] } uuid = { version = "1", features = ["v5"] } diff --git a/src/main.rs b/src/main.rs index a8a9b51..7c1d8a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -426,7 +426,8 @@ fn extract_tool_use(msg: &AssistantMessage) -> Option { None } -const EDIT_INTERVAL_MS: u64 = 5000; +const EDIT_INTERVAL_MS: u64 = 3000; +const DRAFT_INTERVAL_MS: u64 = 1000; const TG_MSG_LIMIT: usize = 4096; async fn invoke_claude_streaming( @@ -452,6 +453,30 @@ async fn invoke_claude_streaming( } } +async fn send_message_draft( + client: &reqwest::Client, + token: &str, + chat_id: i64, + draft_id: i64, + text: &str, +) -> Result<()> { + let url = format!("https://api.telegram.org/bot{token}/sendMessageDraft"); + let resp = client + .post(&url) + .json(&serde_json::json!({ + "chat_id": chat_id, + "draft_id": draft_id, + "text": text, + })) + .send() + .await?; + let body: serde_json::Value = resp.json().await?; + if body["ok"].as_bool() != Some(true) { + anyhow::bail!("sendMessageDraft: {}", body); + } + Ok(()) +} + async fn run_claude_streaming( extra_args: &[&str], prompt: &str, @@ -477,16 +502,19 @@ async fn run_claude_streaming( let stdout = child.stdout.take().unwrap(); let mut lines = tokio::io::BufReader::new(stdout).lines(); - // send placeholder immediately so user knows we're on it - let mut msg_id: Option = match bot.send_message(chat_id, CURSOR).await { - Ok(sent) => Some(sent.id), - Err(_) => None, - }; + // sendMessageDraft for native streaming, with editMessageText fallback + let http = reqwest::Client::new(); + let token = bot.token().to_owned(); + let raw_chat_id = chat_id.0; + let draft_id: i64 = 1; + let mut use_draft = true; + + let mut msg_id: Option = None; let mut last_sent_text = String::new(); let mut last_edit = Instant::now(); let mut final_result = String::new(); let mut is_error = false; - let mut tool_status = String::new(); // current tool use status line + let mut tool_status = String::new(); while let Ok(Some(line)) = lines.next_line().await { let event: StreamEvent = match serde_json::from_str(&line) { @@ -496,48 +524,80 @@ async fn run_claude_streaming( match event.event_type.as_str() { "assistant" => { - if let Some(msg) = &event.message { - // check for tool use — show status - if let Some(status) = extract_tool_use(msg) { - tool_status = format!("[{status}]"); - let display = if last_sent_text.is_empty() { - tool_status.clone() + if let Some(amsg) = &event.message { + // determine display content + let (display_raw, new_text) = + if let Some(status) = extract_tool_use(amsg) { + tool_status = format!("[{status}]"); + let d = if last_sent_text.is_empty() { + tool_status.clone() + } else { + format!("{last_sent_text}\n\n{tool_status}") + }; + (d, None) } else { - format!("{last_sent_text}\n\n{tool_status}") + let text = extract_text(amsg); + if text.is_empty() || text == last_sent_text { + continue; + } + let interval = if use_draft { + DRAFT_INTERVAL_MS + } else { + EDIT_INTERVAL_MS + }; + if last_edit.elapsed().as_millis() < interval as u128 { + continue; + } + tool_status.clear(); + (text.clone(), Some(text)) }; - let display = truncate_for_display(&display); - if let Some(id) = msg_id { - let _ = bot.edit_message_text(chat_id, id, &display).await; - } else if let Ok(sent) = bot.send_message(chat_id, &display).await { - msg_id = Some(sent.id); + let display = truncate_for_display(&display_raw); + + if use_draft { + match send_message_draft( + &http, &token, raw_chat_id, draft_id, &display, + ) + .await + { + Ok(_) => { + if let Some(t) = new_text { + last_sent_text = t; + } + last_edit = Instant::now(); + } + Err(e) => { + warn!("sendMessageDraft failed, falling back: {e:#}"); + use_draft = false; + if let Ok(sent) = + bot.send_message(chat_id, &display).await + { + msg_id = Some(sent.id); + if let Some(t) = new_text { + last_sent_text = t; + } + last_edit = Instant::now(); + } + } } - last_edit = Instant::now(); - continue; - } - - // check for text content - let text = extract_text(msg); - if text.is_empty() || text == last_sent_text { - continue; - } - - // throttle edits - if last_edit.elapsed().as_millis() < EDIT_INTERVAL_MS as u128 { - continue; - } - - tool_status.clear(); - let display = truncate_for_display(&text); - - if let Some(id) = msg_id { - if bot.edit_message_text(chat_id, id, &display).await.is_ok() { - last_sent_text = text; + } else if let Some(id) = msg_id { + if bot + .edit_message_text(chat_id, id, &display) + .await + .is_ok() + { + if let Some(t) = new_text { + last_sent_text = t; + } last_edit = Instant::now(); } - } else if let Ok(sent) = bot.send_message(chat_id, &display).await { + } else if let Ok(sent) = + bot.send_message(chat_id, &display).await + { msg_id = Some(sent.id); - last_sent_text = text; + if let Some(t) = new_text { + last_sent_text = t; + } last_edit = Instant::now(); } } @@ -576,10 +636,12 @@ async fn run_claude_streaming( } else { format!("claude exited: {:?}", status) }; - if let Some(id) = msg_id { - let _ = bot - .edit_message_text(chat_id, id, format!("[error] {err_detail}")) - .await; + if !use_draft { + if let Some(id) = msg_id { + let _ = bot + .edit_message_text(chat_id, id, format!("[error] {err_detail}")) + .await; + } } anyhow::bail!("{err_detail}"); } @@ -588,18 +650,18 @@ async fn run_claude_streaming( return Ok(final_result); } - // final update: replace streaming message with complete result + // final result: send as real message(s) — draft auto-disappears let chunks: Vec<&str> = split_msg(&final_result, TG_MSG_LIMIT); - if let Some(id) = msg_id { - // edit first message with final text + if !use_draft && msg_id.is_some() { + // edit mode: replace streaming message with final text + let id = msg_id.unwrap(); let _ = bot.edit_message_text(chat_id, id, chunks[0]).await; - // send remaining chunks as new messages for chunk in &chunks[1..] { let _ = bot.send_message(chat_id, *chunk).await; } } else { - // never got to send a streaming message, send all now + // draft mode or no existing message: sendMessage replaces the draft for chunk in &chunks { let _ = bot.send_message(chat_id, *chunk).await; }