1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[repr(u8)]
10pub enum ClientFrameType {
11 AuthResponse = 0,
13 UnlockVault = 1,
15 SecretsList = 2,
17 SecretsGet = 3,
19 SecretsStore = 4,
21 SecretsDelete = 5,
23 SecretsPeek = 6,
25 SecretsSetPolicy = 7,
27 SecretsSetDisabled = 8,
29 SecretsDeleteCredential = 9,
31 SecretsHasTotp = 10,
33 SecretsSetupTotp = 11,
35 SecretsVerifyTotp = 12,
37 SecretsRemoveTotp = 13,
39 Reload = 14,
41 Cancel = 15,
43 Chat = 16,
45 ToolApprovalResponse = 17,
47 UserPromptResponse = 18,
49 TasksRequest = 19,
51 ThreadCreate = 20,
53 ThreadSwitch = 21,
55 ThreadList = 22,
57 ThreadClose = 23,
59 ThreadRename = 24,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[repr(u8)]
66pub enum ServerFrameType {
67 AuthChallenge = 0,
69 AuthResult = 1,
71 AuthLocked = 2,
73 Hello = 3,
75 Status = 4,
77 VaultUnlocked = 5,
79 SecretsListResult = 6,
81 SecretsStoreResult = 7,
83 SecretsGetResult = 8,
85 SecretsDeleteResult = 9,
87 SecretsPeekResult = 10,
89 SecretsSetPolicyResult = 11,
91 SecretsSetDisabledResult = 12,
93 SecretsDeleteCredentialResult = 13,
95 SecretsHasTotpResult = 14,
97 SecretsSetupTotpResult = 15,
99 SecretsVerifyTotpResult = 16,
101 SecretsRemoveTotpResult = 17,
103 ReloadResult = 18,
105 Error = 19,
107 Info = 20,
109 StreamStart = 21,
111 Chunk = 22,
113 ThinkingStart = 23,
115 ThinkingDelta = 24,
117 ThinkingEnd = 25,
119 ToolCall = 26,
121 ToolResult = 27,
123 ResponseDone = 28,
125 ToolApprovalRequest = 29,
127 UserPromptRequest = 30,
129 TasksUpdate = 31,
131 ThreadsUpdate = 32,
133 ThreadCreated = 33,
135 ThreadSwitched = 34,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[repr(u8)]
142pub enum StatusType {
143 ModelConfigured = 0,
145 CredentialsLoaded = 1,
147 CredentialsMissing = 2,
149 ModelConnecting = 3,
151 ModelReady = 4,
153 ModelError = 5,
155 NoModel = 6,
157 VaultLocked = 7,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ClientFrame {
168 pub frame_type: ClientFrameType,
169 pub payload: ClientPayload,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub enum ClientPayload {
175 Empty,
176 AuthChallenge {
177 method: String,
178 },
179 AuthResponse {
180 code: String,
181 },
182 UnlockVault {
183 password: String,
184 },
185 Reload,
186 Chat {
187 messages: Vec<super::types::ChatMessage>,
188 },
189 SecretsList,
190 SecretsGet {
191 key: String,
192 },
193 SecretsStore {
194 key: String,
195 value: String,
196 },
197 SecretsDelete {
198 key: String,
199 },
200 SecretsPeek {
201 name: String,
202 },
203 SecretsSetPolicy {
204 name: String,
205 policy: String,
206 skills: Vec<String>,
207 },
208 SecretsSetDisabled {
209 name: String,
210 disabled: bool,
211 },
212 SecretsDeleteCredential {
213 name: String,
214 },
215 SecretsHasTotp,
216 SecretsSetupTotp,
217 SecretsVerifyTotp {
218 code: String,
219 },
220 SecretsRemoveTotp,
221 ToolApprovalResponse {
222 id: String,
223 approved: bool,
224 },
225 UserPromptResponse {
226 id: String,
227 dismissed: bool,
228 value: crate::user_prompt_types::PromptResponseValue,
229 },
230 TasksRequest {
232 session: Option<String>,
233 },
234 ThreadCreate {
236 label: String,
237 },
238 ThreadSwitch {
240 thread_id: u64,
241 },
242 ThreadList,
244 ThreadClose {
246 thread_id: u64,
247 },
248 ThreadRename {
250 thread_id: u64,
251 new_label: String,
252 },
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct ServerFrame {
258 pub frame_type: ServerFrameType,
259 pub payload: ServerPayload,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub enum ServerPayload {
265 Empty,
266 Hello {
267 agent: String,
268 settings_dir: String,
269 vault_locked: bool,
270 provider: Option<String>,
271 model: Option<String>,
272 },
273 AuthChallenge {
274 method: String,
275 },
276 AuthResult {
277 ok: bool,
278 message: Option<String>,
279 retry: Option<bool>,
280 },
281 AuthLocked {
282 message: String,
283 retry_after: Option<u64>,
284 },
285 Status {
286 status: StatusType,
287 detail: String,
288 },
289 VaultUnlocked {
290 ok: bool,
291 message: Option<String>,
292 },
293 SecretsListResult {
294 ok: bool,
295 entries: Vec<SecretEntryDto>,
296 },
297 SecretsStoreResult {
298 ok: bool,
299 message: String,
300 },
301 SecretsGetResult {
302 ok: bool,
303 key: String,
304 value: Option<String>,
305 message: Option<String>,
306 },
307 SecretsDeleteResult {
308 ok: bool,
309 message: Option<String>,
310 },
311 SecretsPeekResult {
312 ok: bool,
313 fields: Vec<(String, String)>,
314 message: Option<String>,
315 },
316 SecretsSetPolicyResult {
317 ok: bool,
318 message: Option<String>,
319 },
320 SecretsSetDisabledResult {
321 ok: bool,
322 message: Option<String>,
323 },
324 SecretsDeleteCredentialResult {
325 ok: bool,
326 message: Option<String>,
327 },
328 SecretsHasTotpResult {
329 has_totp: bool,
330 },
331 SecretsSetupTotpResult {
332 ok: bool,
333 uri: Option<String>,
334 message: Option<String>,
335 },
336 SecretsVerifyTotpResult {
337 ok: bool,
338 message: Option<String>,
339 },
340 SecretsRemoveTotpResult {
341 ok: bool,
342 message: Option<String>,
343 },
344 ReloadResult {
345 ok: bool,
346 provider: String,
347 model: String,
348 message: Option<String>,
349 },
350 Error {
351 ok: bool,
352 message: String,
353 },
354 Info {
355 message: String,
356 },
357 StreamStart,
358 Chunk {
359 delta: String,
360 },
361 ThinkingStart,
362 ThinkingDelta {
363 delta: String,
364 },
365 ThinkingEnd,
366 ToolCall {
367 id: String,
368 name: String,
369 arguments: String,
370 },
371 ToolResult {
372 id: String,
373 name: String,
374 result: String,
375 is_error: bool,
376 },
377 ResponseDone {
378 ok: bool,
379 },
380 ToolApprovalRequest {
381 id: String,
382 name: String,
383 arguments: String,
384 },
385 UserPromptRequest {
386 id: String,
387 prompt: crate::user_prompt_types::UserPrompt,
388 },
389 TasksUpdate {
390 tasks: Vec<TaskInfoDto>,
391 },
392 ThreadsUpdate {
393 threads: Vec<ThreadInfoDto>,
394 foreground_id: Option<u64>,
395 },
396 ThreadCreated {
397 thread_id: u64,
398 label: String,
399 },
400 ThreadSwitched {
401 thread_id: u64,
402 context_summary: Option<String>,
404 },
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct TaskInfoDto {
410 pub id: u64,
411 pub label: String,
412 pub description: Option<String>,
413 pub status: String,
414 pub is_foreground: bool,
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
421pub struct ThreadInfoDto {
422 pub id: u64,
423 pub label: String,
424 pub description: Option<String>,
426 pub status: Option<String>,
428 pub kind_icon: Option<String>,
430 pub status_icon: Option<String>,
432 pub is_foreground: bool,
433 pub message_count: usize,
434 pub has_summary: bool,
435}
436
437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
439pub struct SecretEntryDto {
440 pub name: String,
441 pub label: String,
442 pub kind: String,
443 pub policy: String,
444 pub disabled: bool,
445}
446
447pub fn serialize_frame<T: serde::Serialize>(frame: &T) -> Result<Vec<u8>, String> {
453 bincode::serde::encode_to_vec(frame, bincode::config::standard()).map_err(|e| e.to_string())
454}
455
456pub fn deserialize_frame<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
458 let (result, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())
459 .map_err(|e| e.to_string())?;
460 Ok(result)
461}
462
463#[macro_export]
465macro_rules! send_binary_frame {
466 ($writer:expr, $frame:expr) => {{
467 let bytes = $crate::gateway::serialize_frame(&$frame)
468 .map_err(|e| anyhow::anyhow!("Failed to serialize frame: {}", e))?;
469 $writer
470 .send(tokio_tungstenite::tungstenite::Message::Binary(bytes))
471 .await
472 .map_err(|e| anyhow::anyhow!("Failed to send frame: {}", e))
473 }};
474}
475
476#[macro_export]
478macro_rules! parse_binary_client_frame {
479 ($bytes:expr) => {{
480 $crate::gateway::deserialize_frame::<$crate::gateway::ClientFrame>($bytes)
481 .map_err(|e| anyhow::anyhow!("Failed to parse client frame: {}", e))
482 }};
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 mod serialization {
490 use super::*;
491
492 #[test]
493 fn test_server_frame_type_values() {
494 assert_eq!(ServerFrameType::AuthChallenge as u8, 0);
495 assert_eq!(ServerFrameType::AuthResult as u8, 1);
496 assert_eq!(ServerFrameType::AuthLocked as u8, 2);
497 assert_eq!(ServerFrameType::Hello as u8, 3);
498 assert_eq!(ServerFrameType::Status as u8, 4);
499 assert_eq!(ServerFrameType::VaultUnlocked as u8, 5);
500 assert_eq!(ServerFrameType::SecretsListResult as u8, 6);
501 assert_eq!(ServerFrameType::SecretsStoreResult as u8, 7);
502 assert_eq!(ServerFrameType::SecretsGetResult as u8, 8);
503 assert_eq!(ServerFrameType::SecretsDeleteResult as u8, 9);
504 assert_eq!(ServerFrameType::SecretsPeekResult as u8, 10);
505 assert_eq!(ServerFrameType::SecretsSetPolicyResult as u8, 11);
506 assert_eq!(ServerFrameType::SecretsSetDisabledResult as u8, 12);
507 assert_eq!(ServerFrameType::SecretsDeleteCredentialResult as u8, 13);
508 assert_eq!(ServerFrameType::SecretsHasTotpResult as u8, 14);
509 assert_eq!(ServerFrameType::SecretsSetupTotpResult as u8, 15);
510 assert_eq!(ServerFrameType::SecretsVerifyTotpResult as u8, 16);
511 assert_eq!(ServerFrameType::SecretsRemoveTotpResult as u8, 17);
512 assert_eq!(ServerFrameType::ReloadResult as u8, 18);
513 assert_eq!(ServerFrameType::Error as u8, 19);
514 assert_eq!(ServerFrameType::Info as u8, 20);
515 assert_eq!(ServerFrameType::StreamStart as u8, 21);
516 assert_eq!(ServerFrameType::Chunk as u8, 22);
517 assert_eq!(ServerFrameType::ThinkingStart as u8, 23);
518 assert_eq!(ServerFrameType::ThinkingDelta as u8, 24);
519 assert_eq!(ServerFrameType::ThinkingEnd as u8, 25);
520 assert_eq!(ServerFrameType::ToolCall as u8, 26);
521 assert_eq!(ServerFrameType::ToolResult as u8, 27);
522 assert_eq!(ServerFrameType::ResponseDone as u8, 28);
523 assert_eq!(ServerFrameType::ToolApprovalRequest as u8, 29);
524 assert_eq!(ServerFrameType::UserPromptRequest as u8, 30);
525 }
526
527 #[test]
528 fn test_client_frame_type_values() {
529 assert_eq!(ClientFrameType::AuthResponse as u8, 0);
530 assert_eq!(ClientFrameType::UnlockVault as u8, 1);
531 assert_eq!(ClientFrameType::SecretsList as u8, 2);
532 assert_eq!(ClientFrameType::SecretsGet as u8, 3);
533 assert_eq!(ClientFrameType::SecretsStore as u8, 4);
534 assert_eq!(ClientFrameType::SecretsDelete as u8, 5);
535 assert_eq!(ClientFrameType::SecretsPeek as u8, 6);
536 assert_eq!(ClientFrameType::SecretsSetPolicy as u8, 7);
537 assert_eq!(ClientFrameType::SecretsSetDisabled as u8, 8);
538 assert_eq!(ClientFrameType::SecretsDeleteCredential as u8, 9);
539 assert_eq!(ClientFrameType::SecretsHasTotp as u8, 10);
540 assert_eq!(ClientFrameType::SecretsSetupTotp as u8, 11);
541 assert_eq!(ClientFrameType::SecretsVerifyTotp as u8, 12);
542 assert_eq!(ClientFrameType::SecretsRemoveTotp as u8, 13);
543 assert_eq!(ClientFrameType::Reload as u8, 14);
544 assert_eq!(ClientFrameType::Cancel as u8, 15);
545 assert_eq!(ClientFrameType::Chat as u8, 16);
546 assert_eq!(ClientFrameType::ToolApprovalResponse as u8, 17);
547 assert_eq!(ClientFrameType::UserPromptResponse as u8, 18);
548 }
549
550 #[test]
551 fn test_status_type_values() {
552 assert_eq!(StatusType::ModelConfigured as u8, 0);
553 assert_eq!(StatusType::CredentialsLoaded as u8, 1);
554 assert_eq!(StatusType::CredentialsMissing as u8, 2);
555 assert_eq!(StatusType::ModelConnecting as u8, 3);
556 assert_eq!(StatusType::ModelReady as u8, 4);
557 assert_eq!(StatusType::ModelError as u8, 5);
558 assert_eq!(StatusType::NoModel as u8, 6);
559 assert_eq!(StatusType::VaultLocked as u8, 7);
560 }
561
562 #[test]
563 fn test_server_frame_roundtrip_hello() {
564 let frame = ServerFrame {
565 frame_type: ServerFrameType::Hello,
566 payload: ServerPayload::Hello {
567 agent: "test-agent".into(),
568 settings_dir: "/tmp/settings".into(),
569 vault_locked: false,
570 provider: Some("anthropic".into()),
571 model: Some("claude-3".into()),
572 },
573 };
574
575 let bytes = serialize_frame(&frame).expect("serialize should succeed");
576 let decoded: ServerFrame =
577 deserialize_frame(&bytes).expect("deserialize should succeed");
578
579 match decoded.payload {
580 ServerPayload::Hello {
581 agent,
582 settings_dir,
583 vault_locked,
584 provider,
585 model,
586 } => {
587 assert_eq!(agent, "test-agent");
588 assert_eq!(settings_dir, "/tmp/settings");
589 assert!(!vault_locked);
590 assert_eq!(provider, Some("anthropic".into()));
591 assert_eq!(model, Some("claude-3".into()));
592 }
593 _ => panic!("Expected Hello payload"),
594 }
595 }
596
597 #[test]
598 fn test_server_frame_roundtrip_chunk() {
599 let frame = ServerFrame {
600 frame_type: ServerFrameType::Chunk,
601 payload: ServerPayload::Chunk {
602 delta: "Hello, world!".into(),
603 },
604 };
605
606 let bytes = serialize_frame(&frame).expect("serialize should succeed");
607 let decoded: ServerFrame =
608 deserialize_frame(&bytes).expect("deserialize should succeed");
609
610 match decoded.payload {
611 ServerPayload::Chunk { delta } => {
612 assert_eq!(delta, "Hello, world!");
613 }
614 _ => panic!("Expected Chunk payload"),
615 }
616 }
617
618 #[test]
619 fn test_server_frame_roundtrip_status() {
620 let frame = ServerFrame {
621 frame_type: ServerFrameType::Status,
622 payload: ServerPayload::Status {
623 status: StatusType::ModelReady,
624 detail: "Connected to Claude 3.5 Sonnet".into(),
625 },
626 };
627
628 let bytes = serialize_frame(&frame).expect("serialize should succeed");
629 let decoded: ServerFrame =
630 deserialize_frame(&bytes).expect("deserialize should succeed");
631
632 match decoded.payload {
633 ServerPayload::Status { status, detail } => {
634 assert_eq!(status, StatusType::ModelReady);
635 assert_eq!(detail, "Connected to Claude 3.5 Sonnet");
636 }
637 _ => panic!("Expected Status payload"),
638 }
639 }
640
641 #[test]
642 fn test_server_frame_roundtrip_auth_result() {
643 let frame = ServerFrame {
644 frame_type: ServerFrameType::AuthResult,
645 payload: ServerPayload::AuthResult {
646 ok: true,
647 message: Some("Authenticated successfully".into()),
648 retry: None,
649 },
650 };
651
652 let bytes = serialize_frame(&frame).expect("serialize should succeed");
653 let decoded: ServerFrame =
654 deserialize_frame(&bytes).expect("deserialize should succeed");
655
656 match decoded.payload {
657 ServerPayload::AuthResult { ok, message, retry } => {
658 assert!(ok);
659 assert_eq!(message, Some("Authenticated successfully".into()));
660 assert!(retry.is_none());
661 }
662 _ => panic!("Expected AuthResult payload"),
663 }
664 }
665
666 #[test]
667 fn test_client_frame_roundtrip_chat() {
668 let frame = ClientFrame {
669 frame_type: ClientFrameType::Chat,
670 payload: ClientPayload::Empty,
671 };
672
673 let bytes = serialize_frame(&frame).expect("serialize should succeed");
674 let decoded: ClientFrame =
675 deserialize_frame(&bytes).expect("deserialize should succeed");
676
677 assert_eq!(decoded.frame_type, ClientFrameType::Chat);
678 matches!(decoded.payload, ClientPayload::Empty);
679 }
680
681 #[test]
682 fn test_client_frame_roundtrip_secrets_store() {
683 let frame = ClientFrame {
684 frame_type: ClientFrameType::SecretsStore,
685 payload: ClientPayload::SecretsStore {
686 key: "OPENAI_API_KEY".into(),
687 value: "sk-test123".into(),
688 },
689 };
690
691 let bytes = serialize_frame(&frame).expect("serialize should succeed");
692 let decoded: ClientFrame =
693 deserialize_frame(&bytes).expect("deserialize should succeed");
694
695 match decoded.payload {
696 ClientPayload::SecretsStore { key, value } => {
697 assert_eq!(key, "OPENAI_API_KEY");
698 assert_eq!(value, "sk-test123");
699 }
700 _ => panic!("Expected SecretsStore payload"),
701 }
702 }
703
704 #[test]
705 fn test_secret_entry_dto_roundtrip() {
706 let entry = SecretEntryDto {
707 name: "api_key".into(),
708 label: "OpenAI API Key".into(),
709 kind: "ApiKey".into(),
710 policy: "always".into(),
711 disabled: false,
712 };
713
714 let json = serde_json::to_string(&entry).expect("JSON serialize should succeed");
715 let decoded: SecretEntryDto =
716 serde_json::from_str(&json).expect("JSON deserialize should succeed");
717
718 assert_eq!(decoded.name, "api_key");
719 assert_eq!(decoded.label, "OpenAI API Key");
720 assert_eq!(decoded.kind, "ApiKey");
721 assert_eq!(decoded.policy, "always");
722 assert!(!decoded.disabled);
723 }
724
725 #[test]
726 fn test_user_prompt_response_bincode_roundtrip() {
727 use crate::user_prompt_types::PromptResponseValue;
728
729 let frame = ClientFrame {
730 frame_type: ClientFrameType::UserPromptResponse,
731 payload: ClientPayload::UserPromptResponse {
732 id: "call_456".into(),
733 dismissed: false,
734 value: PromptResponseValue::Text("hello world".into()),
735 },
736 };
737 let bytes = serialize_frame(&frame).expect("serialize should succeed");
738 let decoded: ClientFrame =
739 deserialize_frame(&bytes).expect("deserialize should succeed");
740 match decoded.payload {
741 ClientPayload::UserPromptResponse {
742 id,
743 dismissed,
744 value,
745 } => {
746 assert_eq!(id, "call_456");
747 assert!(!dismissed);
748 assert_eq!(value, PromptResponseValue::Text("hello world".into()));
749 }
750 _ => panic!("Expected UserPromptResponse payload"),
751 }
752 }
753
754 #[test]
755 fn test_server_user_prompt_request_bincode_roundtrip() {
756 use crate::user_prompt_types::{PromptType, UserPrompt};
757
758 let prompt = UserPrompt {
759 id: "call_789".into(),
760 title: "What is your name?".into(),
761 description: Some("Please enter your full name".into()),
762 prompt_type: PromptType::TextInput {
763 placeholder: Some("John Doe".into()),
764 default: None,
765 },
766 };
767
768 let frame = ServerFrame {
769 frame_type: ServerFrameType::UserPromptRequest,
770 payload: ServerPayload::UserPromptRequest {
771 id: "call_789".into(),
772 prompt: prompt.clone(),
773 },
774 };
775
776 let bytes = serialize_frame(&frame).expect("serialize should succeed");
777 let decoded: ServerFrame =
778 deserialize_frame(&bytes).expect("deserialize should succeed");
779
780 assert_eq!(decoded.frame_type, ServerFrameType::UserPromptRequest);
781 match decoded.payload {
782 ServerPayload::UserPromptRequest { id, prompt: p } => {
783 assert_eq!(id, "call_789");
784 assert_eq!(p.title, "What is your name?");
785 assert_eq!(p.description, Some("Please enter your full name".into()));
786 assert!(matches!(p.prompt_type, PromptType::TextInput { .. }));
787 }
788 _ => panic!("Expected UserPromptRequest payload"),
789 }
790 }
791
792 #[test]
793 fn test_client_frame_roundtrip_auth_response() {
794 let frame = ClientFrame {
795 frame_type: ClientFrameType::AuthResponse,
796 payload: ClientPayload::AuthResponse {
797 code: "123456".into(),
798 },
799 };
800
801 let bytes = serialize_frame(&frame).expect("serialize should succeed");
802 let decoded: ClientFrame =
803 deserialize_frame(&bytes).expect("deserialize should succeed");
804
805 assert_eq!(decoded.frame_type, ClientFrameType::AuthResponse);
806 match decoded.payload {
807 ClientPayload::AuthResponse { code } => {
808 assert_eq!(code, "123456");
809 }
810 _ => panic!("Expected AuthResponse payload"),
811 }
812 }
813
814 #[test]
815 fn test_server_frame_roundtrip_auth_challenge() {
816 let frame = ServerFrame {
817 frame_type: ServerFrameType::AuthChallenge,
818 payload: ServerPayload::AuthChallenge {
819 method: "totp".into(),
820 },
821 };
822
823 let bytes = serialize_frame(&frame).expect("serialize should succeed");
824 let decoded: ServerFrame =
825 deserialize_frame(&bytes).expect("deserialize should succeed");
826
827 assert_eq!(decoded.frame_type, ServerFrameType::AuthChallenge);
828 match decoded.payload {
829 ServerPayload::AuthChallenge { method } => {
830 assert_eq!(method, "totp");
831 }
832 _ => panic!("Expected AuthChallenge payload"),
833 }
834 }
835
836 #[test]
837 fn test_server_tool_call_bincode_roundtrip() {
838 let frame = ServerFrame {
839 frame_type: ServerFrameType::ToolCall,
840 payload: ServerPayload::ToolCall {
841 id: "call_001".into(),
842 name: "read_file".into(),
843 arguments: r#"{"path":"/tmp/test"}"#.into(),
844 },
845 };
846
847 let bytes = serialize_frame(&frame).expect("serialize should succeed");
848 let decoded: ServerFrame =
849 deserialize_frame(&bytes).expect("deserialize should succeed");
850
851 match decoded.payload {
852 ServerPayload::ToolCall {
853 id,
854 name,
855 arguments,
856 } => {
857 assert_eq!(id, "call_001");
858 assert_eq!(name, "read_file");
859 assert_eq!(arguments, r#"{"path":"/tmp/test"}"#);
860 }
861 _ => panic!("Expected ToolCall payload"),
862 }
863 }
864 }
865}
866
867#[cfg(test)]
868mod frame_size_tests {
869 use super::*;
870
871 #[test]
872 fn test_threads_update_size() {
873 let thread = ThreadInfoDto {
874 id: 1,
875 label: "Main".to_string(),
876 description: None,
877 status: None,
878 kind_icon: None,
879 status_icon: None,
880 is_foreground: true,
881 message_count: 0,
882 has_summary: false,
883 };
884
885 let frame = ServerFrame {
886 frame_type: ServerFrameType::ThreadsUpdate,
887 payload: ServerPayload::ThreadsUpdate {
888 threads: vec![thread],
889 foreground_id: Some(1),
890 },
891 };
892
893 let bytes = serialize_frame(&frame).unwrap();
894 println!("ThreadsUpdate with 1 thread: {} bytes", bytes.len());
895 println!("Bytes: {:?}", bytes);
896
897 let decoded: ServerFrame = deserialize_frame(&bytes).expect("Round-trip deserialization failed");
901
902 assert!(matches!(decoded.frame_type, ServerFrameType::ThreadsUpdate));
904 if let ServerPayload::ThreadsUpdate { threads, foreground_id } = decoded.payload {
905 assert_eq!(threads.len(), 1);
906 assert_eq!(threads[0].id, 1);
907 assert_eq!(threads[0].label, "Main");
908 assert_eq!(threads[0].description, None);
909 assert_eq!(threads[0].status, None);
910 assert_eq!(threads[0].is_foreground, true);
911 assert_eq!(threads[0].message_count, 0);
912 assert_eq!(threads[0].has_summary, false);
913 assert_eq!(foreground_id, Some(1));
914 } else {
915 panic!("Wrong payload type");
916 }
917 }
918}