@@ -2,7 +2,6 @@ use anyhow::Result;
22use async_trait:: async_trait;
33use serde:: Serialize ;
44use std:: sync:: Arc ;
5- use tokio:: sync:: watch;
65use tracing:: error;
76
87use crate :: acp:: { classify_notification, AcpEvent , ContentBlock , SessionPool } ;
@@ -41,6 +40,10 @@ pub struct SenderContext {
4140 pub display_name : String ,
4241 pub channel : String ,
4342 pub channel_id : String ,
43+ /// Thread identifier, if the message is inside a thread.
44+ /// Slack: thread_ts. Discord: None (threads are separate channels).
45+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
46+ pub thread_id : Option < String > ,
4447 pub is_bot : bool ,
4548}
4649
@@ -57,9 +60,6 @@ pub trait ChatAdapter: Send + Sync + 'static {
5760 /// Send a new message, returns a reference to the sent message.
5861 async fn send_message ( & self , channel : & ChannelRef , content : & str ) -> Result < MessageRef > ;
5962
60- /// Edit an existing message in-place.
61- async fn edit_message ( & self , msg : & MessageRef , content : & str ) -> Result < ( ) > ;
62-
6363 /// Create a thread from a trigger message, returns the thread channel ref.
6464 async fn create_thread (
6565 & self ,
@@ -73,6 +73,17 @@ pub trait ChatAdapter: Send + Sync + 'static {
7373
7474 /// Remove a reaction/emoji from a message.
7575 async fn remove_reaction ( & self , msg : & MessageRef , emoji : & str ) -> Result < ( ) > ;
76+
77+ /// Edit an existing message in-place (for streaming updates).
78+ /// Default: unsupported (send-once only).
79+ async fn edit_message ( & self , _msg : & MessageRef , _content : & str ) -> Result < ( ) > {
80+ Err ( anyhow:: anyhow!( "edit_message not supported" ) )
81+ }
82+
83+ /// Whether this adapter should use streaming edit (true) or send-once (false).
84+ fn use_streaming ( & self ) -> bool {
85+ false
86+ }
7687}
7788
7889// --- AdapterRouter ---
@@ -130,8 +141,6 @@ impl AdapterRouter {
130141 }
131142 }
132143
133- let thinking_msg = adapter. send_message ( thread_channel, "..." ) . await ?;
134-
135144 let thread_key = format ! (
136145 "{}:{}" ,
137146 adapter. platform( ) ,
@@ -144,7 +153,7 @@ impl AdapterRouter {
144153 if let Err ( e) = self . pool . get_or_create ( & thread_key) . await {
145154 let msg = format_user_error ( & e. to_string ( ) ) ;
146155 let _ = adapter
147- . edit_message ( & thinking_msg , & format ! ( "⚠️ {msg}" ) )
156+ . send_message ( thread_channel , & format ! ( "⚠️ {msg}" ) )
148157 . await ;
149158 error ! ( "pool error: {e}" ) ;
150159 return Err ( e) ;
@@ -165,7 +174,6 @@ impl AdapterRouter {
165174 & thread_key,
166175 content_blocks,
167176 thread_channel,
168- & thinking_msg,
169177 reactions. clone ( ) ,
170178 )
171179 . await ;
@@ -190,7 +198,7 @@ impl AdapterRouter {
190198
191199 if let Err ( ref e) = result {
192200 let _ = adapter
193- . edit_message ( & thinking_msg , & format ! ( "⚠️ {e}" ) )
201+ . send_message ( thread_channel , & format ! ( "⚠️ {e}" ) )
194202 . await ;
195203 }
196204
@@ -203,13 +211,12 @@ impl AdapterRouter {
203211 thread_key : & str ,
204212 content_blocks : Vec < ContentBlock > ,
205213 thread_channel : & ChannelRef ,
206- thinking_msg : & MessageRef ,
207214 reactions : Arc < StatusReactionController > ,
208215 ) -> Result < ( ) > {
209216 let adapter = adapter. clone ( ) ;
210217 let thread_channel = thread_channel. clone ( ) ;
211- let msg_ref = thinking_msg. clone ( ) ;
212218 let message_limit = adapter. message_limit ( ) ;
219+ let streaming = adapter. use_streaming ( ) ;
213220
214221 self . pool
215222 . with_connection ( thread_key, |conn| {
@@ -221,57 +228,51 @@ impl AdapterRouter {
221228 let ( mut rx, _) = conn. session_prompt ( content_blocks) . await ?;
222229 reactions. set_thinking ( ) . await ;
223230
224- let initial = if reset {
225- "⚠️ _Session expired, starting fresh..._\n \n ..." . to_string ( )
226- } else {
227- "..." . to_string ( )
228- } ;
229- let ( buf_tx, buf_rx) = watch:: channel ( initial) ;
230-
231231 let mut text_buf = String :: new ( ) ;
232232 let mut tool_lines: Vec < ToolEntry > = Vec :: new ( ) ;
233233
234234 if reset {
235235 text_buf. push_str ( "⚠️ _Session expired, starting fresh..._\n \n " ) ;
236236 }
237237
238- // Spawn edit-streaming task — only edits the single message, never sends new ones.
239- // Long content is truncated during streaming; final multi-message split happens after.
240- let streaming_limit = message_limit. saturating_sub ( 100 ) ;
241- let edit_handle = {
242- let adapter = adapter. clone ( ) ;
243- let msg_ref = msg_ref. clone ( ) ;
244- let mut buf_rx = buf_rx. clone ( ) ;
238+ // Streaming edit: send placeholder, spawn edit loop
239+ let ( buf_tx, placeholder_msg) = if streaming {
240+ let initial = if reset {
241+ "⚠️ _Session expired, starting fresh..._\n \n …" . to_string ( )
242+ } else {
243+ "…" . to_string ( )
244+ } ;
245+ let msg = adapter. send_message ( & thread_channel, & initial) . await ?;
246+ let ( tx, rx) = tokio:: sync:: watch:: channel ( initial) ;
247+ let edit_adapter = adapter. clone ( ) ;
248+ let edit_msg = msg. clone ( ) ;
249+ let limit = message_limit;
250+ let mut buf_rx = rx;
245251 tokio:: spawn ( async move {
246- let mut last_content = String :: new ( ) ;
252+ let mut last = String :: new ( ) ;
247253 loop {
248254 tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 1500 ) ) . await ;
249255 if buf_rx. has_changed ( ) . unwrap_or ( false ) {
250256 let content = buf_rx. borrow_and_update ( ) . clone ( ) ;
251- if content != last_content {
252- let display = if content. chars ( ) . count ( ) > streaming_limit {
253- // Tail-priority: keep the last N chars so user
254- // sees the most recent agent output
255- let total = content. chars ( ) . count ( ) ;
256- let skip = total - streaming_limit;
257- let truncated: String = content. chars ( ) . skip ( skip) . collect ( ) ;
258- format ! ( "…(truncated)\n {truncated}" )
257+ if content != last {
258+ let display = if content. chars ( ) . count ( ) > limit - 100 {
259+ format ! ( "…{}" , format:: truncate_chars_tail( & content, limit - 100 ) )
259260 } else {
260261 content. clone ( )
261262 } ;
262- let _ = adapter . edit_message ( & msg_ref , & display) . await ;
263- last_content = content;
263+ let _ = edit_adapter . edit_message ( & edit_msg , & display) . await ;
264+ last = content;
264265 }
265266 }
266- if buf_rx. has_changed ( ) . is_err ( ) {
267- break ;
268- }
267+ if buf_rx. has_changed ( ) . is_err ( ) { break ; }
269268 }
270- } )
269+ } ) ;
270+ ( Some ( tx) , Some ( msg) )
271+ } else {
272+ ( None , None )
271273 } ;
272274
273275 // Process ACP notifications
274- let mut got_first_text = false ;
275276 let mut response_error: Option < String > = None ;
276277 while let Some ( notification) = rx. recv ( ) . await {
277278 if notification. id . is_some ( ) {
@@ -284,12 +285,10 @@ impl AdapterRouter {
284285 if let Some ( event) = classify_notification ( & notification) {
285286 match event {
286287 AcpEvent :: Text ( t) => {
287- if !got_first_text {
288- got_first_text = true ;
289- }
290288 text_buf. push_str ( & t) ;
291- let _ =
292- buf_tx. send ( compose_display ( & tool_lines, & text_buf, true ) ) ;
289+ if let Some ( tx) = & buf_tx {
290+ let _ = tx. send ( compose_display ( & tool_lines, & text_buf, true ) ) ;
291+ }
293292 }
294293 AcpEvent :: Thinking => {
295294 reactions. set_thinking ( ) . await ;
@@ -307,8 +306,9 @@ impl AdapterRouter {
307306 state : ToolState :: Running ,
308307 } ) ;
309308 }
310- let _ =
311- buf_tx. send ( compose_display ( & tool_lines, & text_buf, true ) ) ;
309+ if let Some ( tx) = & buf_tx {
310+ let _ = tx. send ( compose_display ( & tool_lines, & text_buf, true ) ) ;
311+ }
312312 }
313313 AcpEvent :: ToolDone { id, title, status } => {
314314 reactions. set_thinking ( ) . await ;
@@ -329,19 +329,20 @@ impl AdapterRouter {
329329 state : new_state,
330330 } ) ;
331331 }
332- let _ =
333- buf_tx. send ( compose_display ( & tool_lines, & text_buf, true ) ) ;
332+ if let Some ( tx) = & buf_tx {
333+ let _ = tx. send ( compose_display ( & tool_lines, & text_buf, true ) ) ;
334+ }
334335 }
335336 _ => { }
336337 }
337338 }
338339 }
339340
340341 conn. prompt_done ( ) . await ;
342+ // Stop the edit loop
341343 drop ( buf_tx) ;
342- let _ = edit_handle. await ;
343344
344- // Final edit with complete content
345+ // Build final content
345346 let final_content = compose_display ( & tool_lines, & text_buf, false ) ;
346347 let final_content = if final_content. is_empty ( ) {
347348 if let Some ( err) = response_error {
@@ -356,14 +357,18 @@ impl AdapterRouter {
356357 } ;
357358
358359 let chunks = format:: split_message ( & final_content, message_limit) ;
359- let mut current_msg = msg_ref;
360- for ( i, chunk) in chunks. iter ( ) . enumerate ( ) {
361- if i == 0 {
362- let _ = adapter. edit_message ( & current_msg, chunk) . await ;
363- } else if let Ok ( new_msg) =
364- adapter. send_message ( & thread_channel, chunk) . await
365- {
366- current_msg = new_msg;
360+ if let Some ( msg) = placeholder_msg {
361+ // Streaming: edit first chunk into placeholder, send rest as new messages
362+ if let Some ( first) = chunks. first ( ) {
363+ let _ = adapter. edit_message ( & msg, first) . await ;
364+ }
365+ for chunk in chunks. iter ( ) . skip ( 1 ) {
366+ let _ = adapter. send_message ( & thread_channel, chunk) . await ;
367+ }
368+ } else {
369+ // Send-once: all chunks as new messages
370+ for chunk in & chunks {
371+ let _ = adapter. send_message ( & thread_channel, chunk) . await ;
367372 }
368373 }
369374
0 commit comments