1use rsb_derive::Builder;
6use serde::{Deserialize, Serialize};
7use serde_with::skip_serializing_none;
8use url::Url;
9
10use crate::api::SlackAgentSessionStatus;
11use crate::models::blocks::*;
12use crate::models::*;
13use crate::ratectl::*;
14use crate::SlackClientSession;
15use crate::{ClientResult, SlackClientHttpConnector};
16
17impl<'a, SCHC> SlackClientSession<'a, SCHC>
18where
19 SCHC: SlackClientHttpConnector + Send,
20{
21 pub async fn chat_start_stream(
25 &self,
26 req: &SlackApiChatStartStreamRequest,
27 ) -> ClientResult<SlackApiChatStartStreamResponse> {
28 self.http_session_api
29 .http_post("chat.startStream", req, Some(&SLACK_TIER2_METHOD_CONFIG))
30 .await
31 }
32
33 pub async fn chat_append_stream(
37 &self,
38 req: &SlackApiChatAppendStreamRequest,
39 ) -> ClientResult<SlackApiChatAppendStreamResponse> {
40 self.http_session_api
41 .http_post("chat.appendStream", req, Some(&SLACK_TIER4_METHOD_CONFIG))
42 .await
43 }
44
45 pub async fn chat_stop_stream(
49 &self,
50 req: &SlackApiChatStopStreamRequest,
51 ) -> ClientResult<SlackApiChatStopStreamResponse> {
52 self.http_session_api
53 .http_post("chat.stopStream", req, Some(&SLACK_TIER2_METHOD_CONFIG))
54 .await
55 }
56}
57
58#[skip_serializing_none]
59#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
60pub struct SlackApiChatStartStreamRequest {
61 pub channel: SlackChannelId,
62 pub thread_ts: Option<SlackTs>,
63 pub recipient_user_id: Option<SlackUserId>,
64 pub recipient_team_id: Option<SlackTeamId>,
65 pub markdown_text: Option<String>,
66 pub chunks: Option<Vec<SlackStreamChunk>>,
67 pub task_display_mode: Option<SlackStreamTaskDisplayMode>,
68 pub icon_emoji: Option<SlackEmoji>,
69 pub icon_url: Option<Url>,
70 pub username: Option<String>,
71}
72
73#[skip_serializing_none]
74#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
75pub struct SlackApiChatStartStreamResponse {
76 pub channel: SlackChannelId,
77 pub ts: SlackTs,
78}
79
80#[skip_serializing_none]
81#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
82pub struct SlackApiChatAppendStreamRequest {
83 pub channel: SlackChannelId,
84 pub ts: SlackTs,
85 pub markdown_text: Option<String>,
86 pub chunks: Option<Vec<SlackStreamChunk>>,
87}
88
89#[skip_serializing_none]
90#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
91pub struct SlackApiChatAppendStreamResponse {
92 pub channel: SlackChannelId,
93 pub ts: SlackTs,
94}
95
96#[skip_serializing_none]
97#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
98pub struct SlackApiChatStopStreamRequest {
99 pub channel: SlackChannelId,
100 pub ts: SlackTs,
101 #[serde(flatten)]
102 pub content: SlackMessageContent,
103 pub chunks: Option<Vec<SlackStreamChunk>>,
104 pub session_status: Option<SlackAgentSessionStatus>,
105}
106
107#[skip_serializing_none]
108#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
109pub struct SlackApiChatStopStreamResponse {
110 pub channel: SlackChannelId,
111 pub ts: SlackTs,
112 pub message: Option<SlackMessage>,
113}
114
115#[skip_serializing_none]
118#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum SlackStreamChunk {
121 MarkdownText {
122 text: String,
123 },
124 TaskUpdate {
125 id: SlackTaskId,
126 title: String,
127 hide_title: Option<bool>,
128 icon: Option<SlackTaskCardIcon>,
129 status: SlackTaskCardStatus,
130 details: Option<String>,
131 output: Option<String>,
132 sources: Option<Vec<SlackTaskCardSource>>,
134 },
135 PlanUpdate {
136 title: String,
137 },
138 Blocks {
139 blocks: Vec<SlackBlock>,
140 },
141}
142
143#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum SlackStreamTaskDisplayMode {
148 Timeline,
149 Plan,
150}
151
152#[cfg(test)]
153mod test {
154 use super::*;
155
156 #[test]
157 fn test_slack_api_chat_stream_task_update_chunk_round_trip() {
158 let json = serde_json::json!({
159 "type": "task_update",
160 "id": "t1",
161 "title": "Searching",
162 "hide_title": true,
163 "icon": { "type": "icon", "name": "check" },
164 "status": "in_progress",
165 "sources": [{ "type": "url", "url": "https://example.com/", "text": "Example" }]
166 });
167 let chunk: SlackStreamChunk = serde_json::from_value(json.clone()).unwrap();
168 assert_eq!(
169 chunk,
170 SlackStreamChunk::TaskUpdate {
171 id: "t1".into(),
172 title: "Searching".into(),
173 hide_title: Some(true),
174 icon: Some(SlackTaskCardIcon::new("check".into())),
175 status: SlackTaskCardStatus::InProgress,
176 details: None,
177 output: None,
178 sources: Some(vec![SlackTaskCardSource::Url(SlackUrlSourceElement::new(
179 Url::parse("https://example.com").unwrap(),
180 "Example".into()
181 ))]),
182 }
183 );
184 assert_eq!(serde_json::to_value(&chunk).unwrap(), json);
185 }
186}