Skip to main content

rustyclaw_core/gateway/protocol/
frames.rs

1//! Frame types and serialization for gateway protocol.
2//!
3//! This module contains the shared types used by both client and server.
4
5use serde::{Deserialize, Serialize};
6
7/// Incoming frame types from client to gateway.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[repr(u8)]
10pub enum ClientFrameType {
11    /// Authentication response with TOTP code.
12    AuthResponse = 0,
13    /// Unlock the vault with password.
14    UnlockVault = 1,
15    /// List all secrets.
16    SecretsList = 2,
17    /// Get a specific secret.
18    SecretsGet = 3,
19    /// Store a secret.
20    SecretsStore = 4,
21    /// Delete a secret.
22    SecretsDelete = 5,
23    /// Peek at a credential (display without exposing value).
24    SecretsPeek = 6,
25    /// Set access policy for a credential.
26    SecretsSetPolicy = 7,
27    /// Enable/disable a credential.
28    SecretsSetDisabled = 8,
29    /// Delete a credential entirely.
30    SecretsDeleteCredential = 9,
31    /// Check if TOTP is configured.
32    SecretsHasTotp = 10,
33    /// Set up TOTP for the vault.
34    SecretsSetupTotp = 11,
35    /// Verify a TOTP code.
36    SecretsVerifyTotp = 12,
37    /// Remove TOTP from the vault.
38    SecretsRemoveTotp = 13,
39    /// Reload configuration.
40    Reload = 14,
41    /// Cancel the current tool loop.
42    Cancel = 15,
43    /// Chat message (default).
44    Chat = 16,
45    /// User response to a tool approval request.
46    ToolApprovalResponse = 17,
47    /// User response to a structured prompt (ask_user tool).
48    UserPromptResponse = 18,
49    /// Request current task list.
50    TasksRequest = 19,
51    /// Create a new thread.
52    ThreadCreate = 20,
53    /// Switch to a different thread.
54    ThreadSwitch = 21,
55    /// Request thread list.
56    ThreadList = 22,
57    /// Close/delete a thread.
58    ThreadClose = 23,
59    /// Rename a thread.
60    ThreadRename = 24,
61}
62
63/// Outgoing frame types from gateway to client.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[repr(u8)]
66pub enum ServerFrameType {
67    /// Authentication challenge request.
68    AuthChallenge = 0,
69    /// Authentication result.
70    AuthResult = 1,
71    /// Too many auth attempts, locked out.
72    AuthLocked = 2,
73    /// Hello message on connect.
74    Hello = 3,
75    /// Status update frame.
76    Status = 4,
77    /// Vault unlocked result.
78    VaultUnlocked = 5,
79    /// Secrets list result.
80    SecretsListResult = 6,
81    /// Secrets store result.
82    SecretsStoreResult = 7,
83    /// Secrets get result.
84    SecretsGetResult = 8,
85    /// Secrets delete result.
86    SecretsDeleteResult = 9,
87    /// Secrets peek result.
88    SecretsPeekResult = 10,
89    /// Secrets set policy result.
90    SecretsSetPolicyResult = 11,
91    /// Secrets set disabled result.
92    SecretsSetDisabledResult = 12,
93    /// Secrets delete credential result.
94    SecretsDeleteCredentialResult = 13,
95    /// Secrets has TOTP result.
96    SecretsHasTotpResult = 14,
97    /// Secrets setup TOTP result.
98    SecretsSetupTotpResult = 15,
99    /// Secrets verify TOTP result.
100    SecretsVerifyTotpResult = 16,
101    /// Secrets remove TOTP result.
102    SecretsRemoveTotpResult = 17,
103    /// Reload result.
104    ReloadResult = 18,
105    /// Error frame.
106    Error = 19,
107    /// Info frame.
108    Info = 20,
109    /// Stream start.
110    StreamStart = 21,
111    /// Chunk of response text.
112    Chunk = 22,
113    /// Thinking start (for extended thinking).
114    ThinkingStart = 23,
115    /// Thinking delta (streaming thinking content).
116    ThinkingDelta = 24,
117    /// Thinking end.
118    ThinkingEnd = 25,
119    /// Tool call from model.
120    ToolCall = 26,
121    /// Tool result from execution.
122    ToolResult = 27,
123    /// Response complete.
124    ResponseDone = 28,
125    /// Tool approval request — ask user to approve a tool call.
126    ToolApprovalRequest = 29,
127    /// Structured user prompt request (ask_user tool).
128    UserPromptRequest = 30,
129    /// Task list update.
130    TasksUpdate = 31,
131    /// Thread list update.
132    ThreadsUpdate = 32,
133    /// Thread created result.
134    ThreadCreated = 33,
135    /// Thread switched result.
136    ThreadSwitched = 34,
137}
138
139/// Status frame sub-types.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[repr(u8)]
142pub enum StatusType {
143    /// Model is configured.
144    ModelConfigured = 0,
145    /// Credentials loaded.
146    CredentialsLoaded = 1,
147    /// Credentials missing.
148    CredentialsMissing = 2,
149    /// Model connecting.
150    ModelConnecting = 3,
151    /// Model ready.
152    ModelReady = 4,
153    /// Model error.
154    ModelError = 5,
155    /// No model configured.
156    NoModel = 6,
157    /// Vault is locked.
158    VaultLocked = 7,
159}
160
161// ============================================================================
162// Binary Frame Types
163// ============================================================================
164
165/// Generic client frame envelope.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ClientFrame {
168    pub frame_type: ClientFrameType,
169    pub payload: ClientPayload,
170}
171
172/// Payload variants for client frames.
173#[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    /// Request current task list (optionally filtered by session).
231    TasksRequest {
232        session: Option<String>,
233    },
234    /// Create a new thread.
235    ThreadCreate {
236        label: String,
237    },
238    /// Switch to a different thread.
239    ThreadSwitch {
240        thread_id: u64,
241    },
242    /// Request list of threads.
243    ThreadList,
244    /// Close/delete a thread.
245    ThreadClose {
246        thread_id: u64,
247    },
248    /// Rename a thread.
249    ThreadRename {
250        thread_id: u64,
251        new_label: String,
252    },
253}
254
255/// Generic server frame envelope.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct ServerFrame {
258    pub frame_type: ServerFrameType,
259    pub payload: ServerPayload,
260}
261
262/// Payload variants for server frames.
263#[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        /// Optional summary of the thread being switched to
403        context_summary: Option<String>,
404    },
405}
406
407/// DTO for task info in updates.
408#[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/// DTO for thread info in updates (unified tasks + threads).
418/// NOTE: Do NOT use skip_serializing_if with bincode - it breaks deserialization
419/// since bincode is not self-describing (positional format).
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
421pub struct ThreadInfoDto {
422    pub id: u64,
423    pub label: String,
424    /// Description (for spawned tasks)
425    pub description: Option<String>,
426    /// Task status (None = simple thread, Some = spawned task)
427    pub status: Option<String>,
428    /// Icon for the thread kind (e.g. chat, sub-agent, background, task)
429    pub kind_icon: Option<String>,
430    /// Icon for the thread status (e.g. running, completed, failed)
431    pub status_icon: Option<String>,
432    pub is_foreground: bool,
433    pub message_count: usize,
434    pub has_summary: bool,
435}
436
437/// DTO for secret entries in list results.
438#[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
447// ============================================================================
448// Serialization
449// ============================================================================
450
451/// Serialize a frame to binary using bincode with serde.
452pub 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
456/// Deserialize a frame from binary using bincode with serde.
457pub 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/// Helper to send a ServerFrame as a binary WebSocket message.
464#[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/// Helper to parse a client frame from binary WebSocket message bytes.
477#[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        // With bincode standard config (varint encoding), small values are compact.
898        // 16 bytes is correct for this minimal frame.
899        // Key test: can we deserialize it without error?
900        let decoded: ServerFrame = deserialize_frame(&bytes).expect("Round-trip deserialization failed");
901        
902        // Verify we got the right frame type
903        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}