use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock, broadcast};
use tokio_stream::{Stream, StreamExt};
use tracing::{debug, info, error};
use crate::core::CoderLibError;
use crate::integration::{HostCommand, MessageLevel};
#[derive(Debug, Clone)]
pub struct StreamingChunk {
pub content: Option<String>,
pub tokens: Option<u32>,
pub is_final: bool,
}
pub struct StreamingHandler {
command_sender: Option<mpsc::UnboundedSender<HostCommand>>,
active_sessions: Arc<RwLock<std::collections::HashMap<String, StreamingSession>>>,
event_broadcaster: broadcast::Sender<StreamingEvent>,
config: StreamingConfig,
}
#[derive(Debug, Clone)]
pub struct StreamingSession {
pub id: String,
pub accumulated_response: String,
pub state: StreamingState,
pub start_time: std::time::Instant,
pub last_update: std::time::Instant,
pub token_count: usize,
pub completion_percentage: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum StreamingState {
Starting,
Streaming,
Completed,
Cancelled,
Error(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamingConfig {
pub update_interval_ms: u64,
pub max_buffer_size: usize,
pub show_typing_indicator: bool,
pub show_token_count: bool,
pub show_completion_percentage: bool,
pub debounce_ms: u64,
}
impl Default for StreamingConfig {
fn default() -> Self {
Self {
update_interval_ms: 100,
max_buffer_size: 1000,
show_typing_indicator: true,
show_token_count: true,
show_completion_percentage: true,
debounce_ms: 50,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StreamingEvent {
SessionStarted {
session_id: String,
},
ContentUpdate {
session_id: String,
content: String,
is_complete: bool,
},
TokenUpdate {
session_id: String,
token_count: usize,
},
ProgressUpdate {
session_id: String,
percentage: f32,
},
SessionCompleted {
session_id: String,
final_content: String,
total_tokens: usize,
duration_ms: u64,
},
SessionCancelled {
session_id: String,
},
SessionError {
session_id: String,
error: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayOptions {
pub use_separate_panel: bool,
pub replace_inline: bool,
pub show_diff: bool,
pub allow_interaction: bool,
pub panel_position: PanelPosition,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PanelPosition {
Right,
Bottom,
Overlay,
}
impl Default for DisplayOptions {
fn default() -> Self {
Self {
use_separate_panel: true,
replace_inline: false,
show_diff: true,
allow_interaction: true,
panel_position: PanelPosition::Right,
}
}
}
impl StreamingHandler {
pub fn new(config: StreamingConfig) -> Self {
let (event_broadcaster, _) = broadcast::channel(1000);
Self {
command_sender: None,
active_sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
event_broadcaster,
config,
}
}
pub fn set_command_sender(&mut self, sender: mpsc::UnboundedSender<HostCommand>) {
self.command_sender = Some(sender);
}
pub fn subscribe_to_events(&self) -> broadcast::Receiver<StreamingEvent> {
self.event_broadcaster.subscribe()
}
pub async fn handle_streaming_response<S>(
&self,
session_id: String,
stream: S,
display_options: DisplayOptions,
) -> Result<String, CoderLibError>
where
S: Stream<Item = Result<StreamingChunk, CoderLibError>> + Send + Unpin + 'static,
{
info!("Starting streaming response handler for session: {}", session_id);
let session = StreamingSession {
id: session_id.clone(),
accumulated_response: String::new(),
state: StreamingState::Starting,
start_time: std::time::Instant::now(),
last_update: std::time::Instant::now(),
token_count: 0,
completion_percentage: 0.0,
};
{
let mut sessions = self.active_sessions.write().await;
sessions.insert(session_id.clone(), session);
}
let _ = self.event_broadcaster.send(StreamingEvent::SessionStarted {
session_id: session_id.clone(),
});
self.show_streaming_ui(&session_id, &display_options).await?;
let final_content = self.process_stream(session_id.clone(), stream).await?;
{
let mut sessions = self.active_sessions.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.state = StreamingState::Completed;
session.accumulated_response = final_content.clone();
}
}
let duration = {
let sessions = self.active_sessions.read().await;
sessions.get(&session_id)
.map(|s| s.start_time.elapsed().as_millis() as u64)
.unwrap_or(0)
};
let token_count = {
let sessions = self.active_sessions.read().await;
sessions.get(&session_id)
.map(|s| s.token_count)
.unwrap_or(0)
};
let _ = self.event_broadcaster.send(StreamingEvent::SessionCompleted {
session_id: session_id.clone(),
final_content: final_content.clone(),
total_tokens: token_count,
duration_ms: duration,
});
{
let mut sessions = self.active_sessions.write().await;
sessions.remove(&session_id);
}
info!("Streaming response completed for session: {}", session_id);
Ok(final_content)
}
async fn process_stream<S>(
&self,
session_id: String,
mut stream: S,
) -> Result<String, CoderLibError>
where
S: Stream<Item = Result<StreamingChunk, CoderLibError>> + Send + Unpin,
{
let mut accumulated_content = String::new();
let mut buffer = String::new();
let mut last_update = std::time::Instant::now();
{
let mut sessions = self.active_sessions.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.state = StreamingState::Streaming;
}
}
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(chunk) => {
if let Some(content) = chunk.content {
buffer.push_str(&content);
accumulated_content.push_str(&content);
}
if let Some(tokens) = chunk.tokens {
self.update_token_count(&session_id, tokens).await;
}
let should_update = buffer.len() >= self.config.max_buffer_size
|| last_update.elapsed().as_millis() >= self.config.update_interval_ms as u128;
if should_update && !buffer.is_empty() {
self.update_streaming_content(&session_id, &buffer, false).await?;
buffer.clear();
last_update = std::time::Instant::now();
}
{
let mut sessions = self.active_sessions.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.accumulated_response = accumulated_content.clone();
session.last_update = std::time::Instant::now();
session.token_count += chunk.tokens.unwrap_or(0) as usize;
}
}
}
Err(e) => {
error!("Streaming error for session {}: {}", session_id, e);
{
let mut sessions = self.active_sessions.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.state = StreamingState::Error(e.to_string());
}
}
let _ = self.event_broadcaster.send(StreamingEvent::SessionError {
session_id: session_id.clone(),
error: e.to_string(),
});
return Err(e);
}
}
}
if !buffer.is_empty() {
self.update_streaming_content(&session_id, &buffer, true).await?;
}
Ok(accumulated_content)
}
async fn show_streaming_ui(&self, session_id: &str, options: &DisplayOptions) -> Result<(), CoderLibError> {
if options.use_separate_panel {
self.send_command(HostCommand::ShowDialog {
title: format!("AI Response - {}", session_id),
message: "AI is generating response...".to_string(),
buttons: vec!["Cancel".to_string()],
}).await?;
}
if self.config.show_typing_indicator {
self.send_command(HostCommand::ShowMessage {
message: "AI is typing...".to_string(),
level: MessageLevel::Info,
}).await?;
}
Ok(())
}
async fn update_streaming_content(&self, session_id: &str, content: &str, is_complete: bool) -> Result<(), CoderLibError> {
debug!("Updating streaming content for session: {} (complete: {})", session_id, is_complete);
let _ = self.event_broadcaster.send(StreamingEvent::ContentUpdate {
session_id: session_id.to_string(),
content: content.to_string(),
is_complete,
});
if is_complete {
self.send_command(HostCommand::ShowMessage {
message: "AI response completed".to_string(),
level: MessageLevel::Success,
}).await?;
}
Ok(())
}
async fn update_token_count(&self, session_id: &str, tokens: u32) {
if self.config.show_token_count {
let _ = self.event_broadcaster.send(StreamingEvent::TokenUpdate {
session_id: session_id.to_string(),
token_count: tokens as usize,
});
}
}
pub async fn cancel_session(&self, session_id: &str) -> Result<(), CoderLibError> {
info!("Cancelling streaming session: {}", session_id);
{
let mut sessions = self.active_sessions.write().await;
if let Some(session) = sessions.get_mut(session_id) {
session.state = StreamingState::Cancelled;
}
}
let _ = self.event_broadcaster.send(StreamingEvent::SessionCancelled {
session_id: session_id.to_string(),
});
self.send_command(HostCommand::ShowMessage {
message: "AI response cancelled".to_string(),
level: MessageLevel::Warning,
}).await?;
Ok(())
}
pub async fn get_active_sessions(&self) -> Vec<StreamingSession> {
let sessions = self.active_sessions.read().await;
sessions.values().cloned().collect()
}
async fn send_command(&self, command: HostCommand) -> Result<(), CoderLibError> {
if let Some(sender) = &self.command_sender {
sender.send(command)
.map_err(|e| CoderLibError::Integration(
crate::core::error::IntegrationError::OperationFailed(
format!("Failed to send command: {}", e)
)
))?;
}
Ok(())
}
}