Skip to main content

matrixcode_core/
protocol.rs

1// ============================================================================
2// IPC Protocol for VSCode Extension Integration
3// ============================================================================
4// 
5// This module defines the message format for communication between
6// the VSCode extension and the MatrixCode CLI daemon.
7//
8// Communication flow:
9// 1. VSCode extension spawns CLI with --daemon --json flags
10// 2. Extension sends JSON requests via stdin
11// 3. CLI streams JSON events via stdout (JSON Lines format)
12//
13// Example request:
14//   {"type":"chat","content":"帮我分析这个函数","context":{"file":"src/main.rs"}}
15//
16// Example response (streaming):
17//   {"type":"text","content":"这是一个"}
18//   {"type":"text","content":"简单的函数"}
19//   {"type":"tool_use","id":"tool_1","name":"read","input":{"path":"src/main.rs"}}
20//   {"type":"tool_result","tool_use_id":"tool_1","content":"..."}
21//   {"type":"done","usage":{"input":1234,"output":567}}
22
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26// ============================================================================
27// Client Requests (from VSCode extension)
28// ============================================================================
29
30/// Request from VSCode extension
31#[derive(Debug, Serialize, Deserialize)]
32#[serde(tag = "type")]
33#[serde(rename_all = "snake_case")]
34pub enum ClientRequest {
35    /// Chat message
36    Chat {
37        content: String,
38        #[serde(default)]
39        context: Option<RequestContext>,
40    },
41    
42    /// Quick action on code
43    QuickAction {
44        action: QuickActionType,
45        content: String,
46        #[serde(default)]
47        context: Option<RequestContext>,
48        #[serde(default)]
49        instructions: Option<String>,
50    },
51    
52    /// Start a new session
53    NewSession,
54    
55    /// Get current status
56    Status,
57    
58    /// Memory operations
59    Memory {
60        operation: MemoryOperation,
61    },
62    
63    /// Load a specific session
64    LoadSession {
65        session_id: String,
66    },
67    
68    /// List sessions
69    ListSessions,
70}
71
72/// Types of quick actions
73#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum QuickActionType {
76    Explain,
77    Fix,
78    GenerateTests,
79    Refactor,
80    Optimize,
81    Document,
82    Translate,
83}
84
85/// Memory operations
86#[derive(Debug, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum MemoryOperation {
89    List,
90    Search { query: String },
91    Add { content: String, category: Option<String> },
92    Clear,
93    Stats,
94}
95
96/// Context information from VSCode
97#[derive(Debug, Serialize, Deserialize, Default)]
98pub struct RequestContext {
99    /// Workspace root path
100    #[serde(default)]
101    pub workspace: Option<String>,
102    
103    /// Current file path
104    #[serde(default)]
105    pub file: Option<String>,
106    
107    /// File language ID
108    #[serde(default)]
109    pub language: Option<String>,
110    
111    /// Selected text range
112    #[serde(default)]
113    pub selection: Option<Selection>,
114    
115    /// Diagnostics (errors, warnings) in the selection
116    #[serde(default)]
117    pub diagnostics: Option<Vec<Diagnostic>>,
118    
119    /// Additional context (e.g., related files)
120    #[serde(default)]
121    pub extra_files: Option<Vec<String>>,
122}
123
124/// Text selection in editor
125#[derive(Debug, Serialize, Deserialize)]
126pub struct Selection {
127    pub start: Position,
128    pub end: Position,
129}
130
131/// Position in text
132#[derive(Debug, Serialize, Deserialize)]
133pub struct Position {
134    pub line: u32,
135    pub character: u32,
136}
137
138/// Diagnostic information
139#[derive(Debug, Serialize, Deserialize)]
140pub struct Diagnostic {
141    pub severity: String,
142    pub message: String,
143    pub range: Selection,
144    #[serde(default)]
145    pub source: Option<String>,
146    #[serde(default)]
147    pub code: Option<String>,
148}
149
150// ============================================================================
151// Server Events (streamed to VSCode extension)
152// ============================================================================
153
154/// Event streamed to VSCode extension
155#[derive(Debug, Serialize, Deserialize)]
156#[serde(tag = "type")]
157#[serde(rename_all = "snake_case")]
158pub enum StreamEvent {
159    /// Text content (streaming)
160    Text { content: String },
161    
162    /// Thinking content (extended thinking mode)
163    Thinking { content: String },
164    
165    /// Tool use request
166    ToolUse {
167        id: String,
168        name: String,
169        input: serde_json::Value,
170    },
171    
172    /// Tool execution result
173    ToolResult {
174        tool_use_id: String,
175        content: String,
176        #[serde(default)]
177        success: bool,
178    },
179    
180    /// Server-side web search result (Anthropic)
181    WebSearchResult {
182        tool_use_id: String,
183        content: String,
184    },
185    
186    /// Error occurred
187    Error {
188        message: String,
189        #[serde(default)]
190        code: Option<String>,
191    },
192    
193    /// Request completed
194    Done {
195        #[serde(default)]
196        usage: Option<Usage>,
197    },
198    
199    /// Session started/loaded
200    SessionStarted {
201        session_id: String,
202        #[serde(default)]
203        memory_count: Option<usize>,
204    },
205    
206    /// Status response
207    StatusResponse {
208        session_id: Option<String>,
209        message_count: usize,
210        total_tokens: u64,
211        is_streaming: bool,
212    },
213    
214    /// Memory list response
215    MemoryList {
216        memories: Vec<MemoryEntry>,
217    },
218    
219    /// Memory stats response
220    MemoryStats {
221        total: usize,
222        by_category: HashMap<String, usize>,
223    },
224    
225    /// Session list response
226    SessionList {
227        sessions: Vec<SessionInfo>,
228    },
229    
230    /// New memory added
231    MemoryAdded {
232        category: String,
233        content: String,
234    },
235    
236    /// Log message (for debugging)
237    Log {
238        level: String,
239        message: String,
240    },
241}
242
243/// Token usage information
244#[derive(Debug, Serialize, Deserialize)]
245pub struct Usage {
246    pub input: u64,
247    pub output: u64,
248    #[serde(default)]
249    pub cache_read: Option<u64>,
250    #[serde(default)]
251    pub cache_write: Option<u64>,
252}
253
254/// Memory entry
255#[derive(Debug, Serialize, Deserialize)]
256pub struct MemoryEntry {
257    pub id: String,
258    pub category: String,
259    pub content: String,
260    pub created_at: String,
261    #[serde(default)]
262    pub project: Option<String>,
263}
264
265/// Session information
266#[derive(Debug, Serialize, Deserialize)]
267pub struct SessionInfo {
268    pub id: String,
269    #[serde(default)]
270    pub name: Option<String>,
271    pub created_at: String,
272    pub message_count: usize,
273    #[serde(default)]
274    pub last_used: Option<String>,
275}
276
277// ============================================================================
278// Helper functions
279// ============================================================================
280
281impl StreamEvent {
282    /// Create a text event
283    pub fn text(content: impl Into<String>) -> Self {
284        StreamEvent::Text { content: content.into() }
285    }
286    
287    /// Create a thinking event
288    pub fn thinking(content: impl Into<String>) -> Self {
289        StreamEvent::Thinking { content: content.into() }
290    }
291    
292    /// Create a tool use event
293    pub fn tool_use(id: impl Into<String>, name: impl Into<String>, input: serde_json::Value) -> Self {
294        StreamEvent::ToolUse {
295            id: id.into(),
296            name: name.into(),
297            input,
298        }
299    }
300    
301    /// Create a tool result event
302    pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>, success: bool) -> Self {
303        StreamEvent::ToolResult {
304            tool_use_id: tool_use_id.into(),
305            content: content.into(),
306            success,
307        }
308    }
309    
310    /// Create an error event
311    pub fn error(message: impl Into<String>) -> Self {
312        StreamEvent::Error { message: message.into(), code: None }
313    }
314    
315    /// Create a done event
316    pub fn done(usage: Option<Usage>) -> Self {
317        StreamEvent::Done { usage }
318    }
319    
320    /// Create a session started event
321    pub fn session_started(session_id: impl Into<String>, memory_count: Option<usize>) -> Self {
322        StreamEvent::SessionStarted {
323            session_id: session_id.into(),
324            memory_count,
325        }
326    }
327    
328    /// Serialize to JSON line
329    pub fn to_json_line(&self) -> String {
330        serde_json::to_string(self).unwrap_or_default() + "\n"
331    }
332}
333
334impl Usage {
335    /// Create usage from token counts
336    pub fn new(input: u64, output: u64) -> Self {
337        Usage { input, output, cache_read: None, cache_write: None }
338    }
339    
340    /// Create usage with cache information
341    pub fn with_cache(input: u64, output: u64, cache_read: u64, cache_write: u64) -> Self {
342        Usage {
343            input,
344            output,
345            cache_read: Some(cache_read),
346            cache_write: Some(cache_write),
347        }
348    }
349}
350
351// ============================================================================
352// Tests
353// ============================================================================
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    
359    #[test]
360    fn test_serialize_chat_request() {
361        let request = ClientRequest::Chat {
362            content: "Hello".to_string(),
363            context: None,
364        };
365        let json = serde_json::to_string(&request).unwrap();
366        assert!(json.contains("\"type\":\"chat\""));
367        assert!(json.contains("\"content\":\"Hello\""));
368    }
369    
370    #[test]
371    fn test_deserialize_chat_request() {
372        let json = "{\"type\":\"chat\",\"content\":\"Hello\",\"context\":null}";
373        let request: ClientRequest = serde_json::from_str(json).unwrap();
374        match request {
375            ClientRequest::Chat { content, .. } => {
376                assert_eq!(content, "Hello");
377            }
378            _ => panic!("Expected Chat request"),
379        }
380    }
381    
382    #[test]
383    fn test_serialize_stream_event() {
384        let event = StreamEvent::text("Hello world");
385        let json = event.to_json_line();
386        assert!(json.contains("\"type\":\"text\""));
387        assert!(json.contains("\"content\":\"Hello world\""));
388        assert!(json.ends_with("\n"));
389    }
390    
391    #[test]
392    fn test_serialize_tool_use() {
393        let event = StreamEvent::tool_use("tool_1", "read", serde_json::json!({"path": "src/main.rs"}));
394        let json = event.to_json_line();
395        assert!(json.contains("\"type\":\"tool_use\""));
396        assert!(json.contains("\"id\":\"tool_1\""));
397        assert!(json.contains("\"name\":\"read\""));
398    }
399    
400    #[test]
401    fn test_request_context_with_file() {
402        let json = "{\"workspace\":\"/project\",\"file\":\"src/main.rs\",\"language\":\"rust\"}";
403        let context: RequestContext = serde_json::from_str(json).unwrap();
404        assert_eq!(context.workspace, Some("/project".to_string()));
405        assert_eq!(context.file, Some("src/main.rs".to_string()));
406        assert_eq!(context.language, Some("rust".to_string()));
407    }
408    
409    #[test]
410    fn test_quick_action_request() {
411        let json = "{\"type\":\"quick_action\",\"action\":\"explain\",\"content\":\"fn main(){}\",\"context\":{\"language\":\"rust\"}}";
412        let request: ClientRequest = serde_json::from_str(json).unwrap();
413        match request {
414            ClientRequest::QuickAction { action, content, .. } => {
415                assert_eq!(action, QuickActionType::Explain);
416                assert_eq!(content, "fn main(){}");
417            }
418            _ => panic!("Expected QuickAction request"),
419        }
420    }
421}