- Add sender_type field to TypingEvent (user/ai) - Change Redis TTL from 10s to 60s for AI typing persistence - Broadcast typing.start/stop with sender_type=ai when AI stream starts/ends - Replay active AI typing events from Redis on new WS subscribe - Fix ai.stream_chunk WS payload missing display_name and chunk_type - Add initial thinking chunk on AI stream start for immediate indicator
146 lines
4.4 KiB
Rust
146 lines
4.4 KiB
Rust
//! Message types shared between producer and worker.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RoomMessageEnvelope {
|
|
pub id: Uuid,
|
|
pub dedup_key: Option<String>,
|
|
pub room_id: Uuid,
|
|
pub sender_type: String,
|
|
pub sender_id: Option<Uuid>,
|
|
/// AI model ID — set when sender_type = "ai", used for display name lookups.
|
|
pub model_id: Option<Uuid>,
|
|
pub thread_id: Option<Uuid>,
|
|
pub in_reply_to: Option<Uuid>,
|
|
pub content: String,
|
|
pub content_type: String,
|
|
pub send_at: DateTime<Utc>,
|
|
pub seq: i64,
|
|
/// Pre-resolved display name for the sender (e.g. AI model name).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub display_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RoomMessageEvent {
|
|
pub id: Uuid,
|
|
pub room_id: Uuid,
|
|
pub sender_type: String,
|
|
pub sender_id: Option<Uuid>,
|
|
pub thread_id: Option<Uuid>,
|
|
pub in_reply_to: Option<Uuid>,
|
|
pub content: String,
|
|
pub content_type: String,
|
|
pub send_at: DateTime<Utc>,
|
|
pub seq: i64,
|
|
pub display_name: Option<String>,
|
|
/// Present when this event carries reaction updates for the message.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub reactions: Option<Vec<ReactionGroup>>,
|
|
/// Target message ID for reaction update events.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub message_id: Option<Uuid>,
|
|
}
|
|
|
|
/// Typing indicator event — broadcast to all room members.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TypingEvent {
|
|
pub room_id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub username: String,
|
|
pub avatar_url: Option<String>,
|
|
/// "start" or "stop"
|
|
pub action: String,
|
|
/// Sender type: "user" or "ai". Defaults to "user" if absent.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub sender_type: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ReactionGroup {
|
|
pub emoji: String,
|
|
pub count: i32,
|
|
pub reacted_by_me: bool,
|
|
/// Stored as strings (UUIDs) to match the frontend's `users: string[]` type.
|
|
pub users: Vec<String>,
|
|
}
|
|
|
|
impl From<RoomMessageEnvelope> for RoomMessageEvent {
|
|
fn from(e: RoomMessageEnvelope) -> Self {
|
|
Self {
|
|
id: e.id,
|
|
room_id: e.room_id,
|
|
sender_type: e.sender_type,
|
|
sender_id: e.sender_id,
|
|
thread_id: e.thread_id,
|
|
in_reply_to: e.in_reply_to,
|
|
content: e.content,
|
|
content_type: e.content_type,
|
|
send_at: e.send_at,
|
|
seq: e.seq,
|
|
display_name: e.display_name,
|
|
reactions: None,
|
|
message_id: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ProjectRoomEvent {
|
|
pub event_type: String,
|
|
pub project_id: Uuid,
|
|
pub room_id: Option<Uuid>,
|
|
pub category_id: Option<Uuid>,
|
|
pub message_id: Option<Uuid>,
|
|
pub seq: Option<i64>,
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RoomMessageStreamChunkEvent {
|
|
pub message_id: Uuid,
|
|
pub room_id: Uuid,
|
|
pub content: String,
|
|
pub done: bool,
|
|
pub error: Option<String>,
|
|
/// Human-readable AI model name (e.g. "Claude 3.5 Sonnet") for display.
|
|
pub display_name: Option<String>,
|
|
/// What kind of content this chunk contains: "thinking", "answer", "tool_call", "tool_result".
|
|
pub chunk_type: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EmailEnvelope {
|
|
pub id: Uuid,
|
|
pub to: String,
|
|
pub subject: String,
|
|
pub body: String,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Agent task event pushed via Redis Pub/Sub to notify WebSocket clients.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentTaskEvent {
|
|
/// Task ID
|
|
pub task_id: i64,
|
|
/// Project this task belongs to.
|
|
pub project_id: Uuid,
|
|
/// Parent task ID (null for root tasks).
|
|
pub parent_id: Option<i64>,
|
|
/// Event type: started | progress | done | failed | child_done
|
|
pub event: String,
|
|
/// Human-readable progress/status text.
|
|
pub message: Option<String>,
|
|
/// Task output (only on done event).
|
|
pub output: Option<String>,
|
|
/// Error message (only on failed event).
|
|
pub error: Option<String>,
|
|
/// Current status.
|
|
pub status: String,
|
|
/// Timestamp.
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|