Skip to main content

oxi/rpc_mode/
protocol.rs

1//! JSONL framing, JSON-RPC 2.0 types, RPC command/response/event types,
2//! and serialization helpers.
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::io::Write;
8use std::sync::Arc;
9
10// ============================================================================
11// JSONL Framing
12// ============================================================================
13
14/// Serialize a value as a strict JSONL record (JSON + LF).
15///
16/// Framing is LF-only. Payload strings may contain other Unicode separators
17/// such as U+2028 and U+2029. Clients must split records on `\n` only.
18pub fn serialize_json_line(value: &Value) -> String {
19    format!("{}\n", serde_json::to_string(value).unwrap_or_default())
20}
21
22/// Serialize a serializable value as a JSONL record.
23pub fn serialize_json_line_obj<T: Serialize>(value: &T) -> String {
24    match serde_json::to_string(value) {
25        Ok(s) => format!("{}\n", s),
26        Err(e) => {
27            tracing::error!("Failed to serialize JSONL: {}", e);
28            "{\"type\":\"response\",\"command\":\"internal\",\"success\":false,\"error\":\"Serialization error\"}\n".to_string()
29        }
30    }
31}
32
33/// Parse a JSONL line, trimming any trailing CR.
34pub fn parse_json_line(line: &str) -> Result<Value, serde_json::Error> {
35    let trimmed = line.trim_end_matches('\r');
36    serde_json::from_str(trimmed)
37}
38
39// ============================================================================
40// JSON-RPC 2.0 Types
41// ============================================================================
42
43/// JSON-RPC 2.0 request
44#[derive(Debug, Clone, Deserialize)]
45pub struct JsonRpcRequest {
46    /// JSON-RPC protocol version.
47    pub jsonrpc: String, // must be "2.0"
48    #[serde(default)]
49    /// Request correlation ID.
50    pub id: Option<Value>,
51    /// JSON-RPC method name.
52    pub method: String,
53    #[serde(default)]
54    /// Method parameters.
55    pub params: Option<Value>,
56}
57
58/// JSON-RPC 2.0 success response
59#[derive(Debug, Clone, Serialize)]
60pub struct JsonRpcSuccessResponse {
61    /// JSON-RPC protocol version.
62    pub jsonrpc: String,
63    /// Request correlation ID.
64    pub id: Value,
65    /// Result value.
66    pub result: Value,
67}
68
69/// JSON-RPC 2.0 error object
70#[derive(Debug, Clone, Serialize)]
71pub struct JsonRpcError {
72    /// Error code.
73    pub code: i64,
74    /// Error message.
75    pub message: String,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    /// Response data.
78    pub data: Option<Value>,
79}
80
81/// JSON-RPC 2.0 error response
82#[derive(Debug, Clone, Serialize)]
83pub struct JsonRpcErrorResponse {
84    /// JSON-RPC protocol version.
85    pub jsonrpc: String,
86    /// Request correlation ID.
87    pub id: Value,
88    /// Error message.
89    pub error: JsonRpcError,
90}
91
92// Standard JSON-RPC error codes
93/// JSON-RPC parse error code.
94pub const JSONRPC_PARSE_ERROR: i64 = -32700;
95/// JSON-RPC invalid request error code.
96pub const JSONRPC_INVALID_REQUEST: i64 = -32600;
97/// JSON-RPC method-not-found error code.
98pub const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
99/// JSON-RPC invalid params error code.
100pub const JSONRPC_INVALID_PARAMS: i64 = -32602;
101/// JSON-RPC internal error code.
102pub const JSONRPC_INTERNAL_ERROR: i64 = -32603;
103
104// ============================================================================
105// RPC Types
106// ============================================================================
107
108/// RPC command from client
109#[derive(Debug, Clone, Deserialize)]
110#[serde(tag = "type", rename_all = "snake_case")]
111pub enum RpcCommand {
112    // ── Prompting ───────────────────────────────────────────────────
113    /// Send a prompt message to the agent.
114    Prompt {
115        /// Optional client-side request correlation ID.
116        id: Option<String>,
117        /// The user's prompt text.
118        message: String,
119        /// Optional inline images attached to the prompt.
120        images: Option<Vec<ImageData>>,
121        #[serde(default)]
122        /// Desired streaming behavior (e.g. "full", "partial").
123        streaming_behavior: Option<String>,
124    },
125    /// Send a steering message to interrupt the current stream.
126    Steer {
127        /// Optional client-side request correlation ID.
128        id: Option<String>,
129        /// The steering instruction text.
130        message: String,
131        /// Optional images attached to the steering message.
132        images: Option<Vec<ImageData>>,
133    },
134    /// Send a follow-up message to be processed after the current stream.
135    FollowUp {
136        /// Optional client-side request correlation ID.
137        id: Option<String>,
138        /// The follow-up message text.
139        message: String,
140        /// Optional images attached to the follow-up.
141        images: Option<Vec<ImageData>>,
142    },
143    /// Abort the current agent response.
144    Abort {
145        /// Optional client-side request correlation ID.
146        id: Option<String>,
147    },
148    /// Create a new session.
149    NewSession {
150        /// Optional client-side request correlation ID.
151        id: Option<String>,
152        /// Optional parent session ID for forked sessions.
153        parent_session: Option<String>,
154    },
155
156    // ── State ───────────────────────────────────────────────────────
157    /// Get the current agent state.
158    GetState {
159        /// Optional client-side request correlation ID.
160        id: Option<String>,
161    },
162
163    // ── Model ──────────────────────────────────────────────────────
164    /// Set the active model.
165    SetModel {
166        /// Optional client-side request correlation ID.
167        id: Option<String>,
168        /// Provider name (e.g. "anthropic").
169        provider: String,
170        /// Model identifier (e.g. "claude-sonnet-4-5").
171        model_id: String,
172    },
173    /// Cycle to the next available model.
174    CycleModel {
175        /// Optional client-side request correlation ID.
176        id: Option<String>,
177    },
178    /// List all available models.
179    GetAvailableModels {
180        /// Optional client-side request correlation ID.
181        id: Option<String>,
182    },
183
184    // ── Thinking ────────────────────────────────────────────────────
185    /// Set the thinking level.
186    SetThinkingLevel {
187        /// Optional client-side request correlation ID.
188        id: Option<String>,
189        /// Thinking level (e.g. "off", "low", "medium", "high", "xhigh").
190        level: String,
191    },
192    /// Cycle to the next thinking level.
193    CycleThinkingLevel {
194        /// Optional client-side request correlation ID.
195        id: Option<String>,
196    },
197
198    // ── Queue modes ─────────────────────────────────────────────────
199    /// Set the steering mode.
200    SetSteeringMode {
201        /// Optional client-side request correlation ID.
202        id: Option<String>,
203        /// Steering mode (e.g. "all", "system").
204        mode: String,
205    },
206    /// Set the follow-up mode.
207    SetFollowUpMode {
208        /// Optional client-side request correlation ID.
209        id: Option<String>,
210        /// Follow-up mode (e.g. "all", "system").
211        mode: String,
212    },
213
214    // ── Compaction ──────────────────────────────────────────────────
215    /// Manually trigger compaction.
216    Compact {
217        /// Optional client-side request correlation ID.
218        id: Option<String>,
219        /// Optional custom instructions for the compaction prompt.
220        custom_instructions: Option<String>,
221    },
222    /// Enable or disable auto-compaction.
223    SetAutoCompaction {
224        /// Optional client-side request correlation ID.
225        id: Option<String>,
226        /// Whether auto-compaction is enabled.
227        enabled: bool,
228    },
229
230    // ── Retry ───────────────────────────────────────────────────────
231    /// Enable or disable auto-retry.
232    SetAutoRetry {
233        /// Optional client-side request correlation ID.
234        id: Option<String>,
235        /// Whether auto-retry is enabled.
236        enabled: bool,
237    },
238    /// Abort the current retry attempt.
239    AbortRetry {
240        /// Optional client-side request correlation ID.
241        id: Option<String>,
242    },
243
244    // ── Bash ────────────────────────────────────────────────────────
245    /// Execute a bash command.
246    Bash {
247        /// Optional client-side request correlation ID.
248        id: Option<String>,
249        /// The shell command to execute.
250        command: String,
251    },
252    /// Abort the running bash command.
253    AbortBash {
254        /// Optional client-side request correlation ID.
255        id: Option<String>,
256    },
257
258    // ── Session ────────────────────────────────────────────────────
259    /// Get session statistics.
260    GetSessionStats {
261        /// Optional client-side request correlation ID.
262        id: Option<String>,
263    },
264    /// Export the session as HTML.
265    ExportHtml {
266        /// Optional client-side request correlation ID.
267        id: Option<String>,
268        /// Filesystem path for the exported HTML file.
269        output_path: Option<String>,
270    },
271    /// Switch to a different session.
272    SwitchSession {
273        /// Optional client-side request correlation ID.
274        id: Option<String>,
275        /// Path to the session file to switch to.
276        session_path: String,
277    },
278    /// Fork the session at a given entry.
279    Fork {
280        /// Optional client-side request correlation ID.
281        id: Option<String>,
282        /// ID of the entry at which to fork.
283        entry_id: String,
284    },
285    /// Clone the current session.
286    Clone {
287        /// Optional client-side request correlation ID.
288        id: Option<String>,
289    },
290    /// Get messages from the forked session.
291    GetForkMessages {
292        /// Optional client-side request correlation ID.
293        id: Option<String>,
294    },
295    /// Get the last assistant text response.
296    GetLastAssistantText {
297        /// Optional client-side request correlation ID.
298        id: Option<String>,
299    },
300    /// Set the session display name.
301    SetSessionName {
302        /// Optional client-side request correlation ID.
303        id: Option<String>,
304        /// New display name for the session.
305        name: String,
306    },
307
308    // ── Messages ───────────────────────────────────────────────────
309    /// Get all session messages.
310    GetMessages {
311        /// Optional client-side request correlation ID.
312        id: Option<String>,
313    },
314
315    // ── Commands ────────────────────────────────────────────────────
316    /// get commands.
317    GetCommands {
318        /// Optional client-side request correlation ID.
319        id: Option<String>,
320    },
321}
322
323/// Image data for RPC commands
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ImageData {
326    /// Base64-encoded image data or URL source.
327    pub source: String,
328    #[serde(rename = "type")]
329    /// Image media type (e.g. "image/png").
330    pub media_type: String,
331}
332
333/// Base64-encoded image data
334#[derive(Debug, Clone)]
335pub struct RpcImageSource {
336    /// Decoded image bytes.
337    pub data: Vec<u8>,
338    /// MIME type (e.g. "image/png").
339    pub mime_type: String,
340}
341
342/// RPC response to client
343#[derive(Debug, Clone, Serialize)]
344#[serde(tag = "type", rename_all = "snake_case")]
345pub enum RpcResponse {
346    /// response.
347    Response {
348        /// Correlation ID matching the original request.
349        id: Option<String>,
350        /// Name of the command this responds to.
351        command: String,
352        /// Whether the command succeeded.
353        success: bool,
354        #[serde(skip_serializing_if = "Option::is_none")]
355        /// Response payload on success.
356        data: Option<Value>,
357        #[serde(skip_serializing_if = "Option::is_none")]
358        /// Error message on failure.
359        error: Option<String>,
360    },
361    /// extension ui request.
362    ExtensionUiRequest(RpcExtensionUiRequest),
363}
364
365/// Extension UI request
366#[derive(Debug, Clone, Serialize)]
367#[serde(tag = "method", rename_all = "snake_case")]
368pub enum RpcExtensionUiRequest {
369    /// Present a single-select menu to the user.
370    Select {
371        /// Unique request identifier.
372        id: String,
373        /// Dialog title.
374        title: String,
375        /// Available choices.
376        options: Vec<String>,
377        #[serde(skip_serializing_if = "Option::is_none")]
378        /// Timeout in seconds.
379        timeout: Option<u64>,
380    },
381    /// Ask the user for a yes/no confirmation.
382    Confirm {
383        /// Unique request identifier.
384        id: String,
385        /// Dialog title.
386        title: String,
387        /// Confirmation prompt text.
388        message: String,
389        #[serde(skip_serializing_if = "Option::is_none")]
390        /// Timeout in seconds.
391        timeout: Option<u64>,
392    },
393    /// Ask the user for a free-form text input.
394    Input {
395        /// Unique request identifier.
396        id: String,
397        /// Dialog title.
398        title: String,
399        #[serde(skip_serializing_if = "Option::is_none")]
400        /// Placeholder text for the input field.
401        placeholder: Option<String>,
402        #[serde(skip_serializing_if = "Option::is_none")]
403        /// Timeout in seconds.
404        timeout: Option<u64>,
405    },
406    /// Open a full-screen editor for multi-line text input.
407    Editor {
408        /// Unique request identifier.
409        id: String,
410        /// Dialog title.
411        title: String,
412        #[serde(skip_serializing_if = "Option::is_none")]
413        /// Pre-filled editor content.
414        prefill: Option<String>,
415    },
416    /// Display a non-blocking notification to the user.
417    Notify {
418        /// Unique request identifier.
419        id: String,
420        /// Notification body text.
421        message: String,
422        #[serde(skip_serializing_if = "Option::is_none")]
423        /// Notification type hint (e.g. "info", "warning", "error").
424        notify_type: Option<String>,
425    },
426    /// Set a status bar entry.
427    SetStatus {
428        /// Unique request identifier.
429        id: String,
430        /// Status bar key for deduplication.
431        status_key: String,
432        /// Status text to display; `None` clears the entry.
433        status_text: Option<String>,
434    },
435    /// Set or update a custom widget in the UI.
436    SetWidget {
437        /// Unique request identifier.
438        id: String,
439        /// Widget key for deduplication.
440        widget_key: String,
441        /// Lines of content to render.
442        widget_lines: Option<Vec<String>>,
443        #[serde(skip_serializing_if = "Option::is_none")]
444        /// Widget placement hint (e.g. "bottom", "sidebar").
445        widget_placement: Option<String>,
446    },
447    /// Set the window/terminal title.
448    SetTitle {
449        /// Unique request identifier.
450        id: String,
451        /// New title text.
452        title: String,
453    },
454    /// Replace the text in the user's input editor.
455    SetEditorText {
456        /// Unique request identifier.
457        id: String,
458        /// New editor text content.
459        text: String,
460    },
461}
462
463/// Extension UI response from client
464#[derive(Debug, Clone, Deserialize)]
465#[serde(tag = "type", rename_all = "snake_case")]
466pub enum RpcExtensionUiResponse {
467    /// Respond to an extension UI request.
468    ExtensionUiResponse {
469        /// The request ID being responded to.
470        id: String,
471        #[serde(default)]
472        /// The user's text input (for Input/Editor requests).
473        value: Option<String>,
474        #[serde(default)]
475        /// Whether the user confirmed (for Confirm requests).
476        confirmed: Option<bool>,
477        #[serde(default)]
478        /// Whether the user dismissed the request without answering.
479        cancelled: Option<bool>,
480    },
481}
482
483// ============================================================================
484// State / Info Types
485// ============================================================================
486
487/// Session state for get_state response
488#[derive(Debug, Clone, Serialize)]
489pub struct SessionState {
490    /// Current model info
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub model: Option<ModelInfo>,
493    /// thinking level.
494    pub thinking_level: String,
495    /// is streaming.
496    pub is_streaming: bool,
497    /// is compacting.
498    pub is_compacting: bool,
499    /// steering mode.
500    pub steering_mode: String,
501    /// follow up mode.
502    pub follow_up_mode: String,
503    /// Session file path
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub session_file: Option<String>,
506    /// Session identifier.
507    pub session_id: String,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    /// session name.
510    pub session_name: Option<String>,
511    /// auto compaction enabled.
512    pub auto_compaction_enabled: bool,
513    /// message count.
514    pub message_count: usize,
515    /// pending message count.
516    pub pending_message_count: usize,
517}
518
519/// Model information for session state
520#[derive(Debug, Clone, Serialize)]
521pub struct ModelInfo {
522    /// Provider name.
523    pub provider: String,
524    /// Request correlation ID.
525    pub id: String,
526}
527
528/// Command info for get_commands response
529#[derive(Debug, Clone, Serialize)]
530pub struct CommandInfo {
531    /// Name value.
532    pub name: String,
533    #[serde(skip_serializing_if = "Option::is_none")]
534    /// description.
535    pub description: Option<String>,
536    /// source.
537    pub source: String,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    /// source info.
540    pub source_info: Option<SourceInfo>,
541}
542
543/// Source info for command provenance
544#[derive(Debug, Clone, Serialize)]
545pub struct SourceInfo {
546    #[serde(skip_serializing_if = "Option::is_none")]
547    /// File path.
548    pub path: Option<String>,
549    #[serde(skip_serializing_if = "Option::is_none")]
550    /// origin.
551    pub origin: Option<String>,
552}
553
554/// Result of a session stat query
555#[derive(Debug, Clone, Serialize)]
556pub struct SessionStats {
557    /// message count.
558    pub message_count: usize,
559    /// token count.
560    pub token_count: Option<usize>,
561    /// last activity.
562    pub last_activity: Option<i64>,
563}
564
565/// Result of compaction
566#[derive(Debug, Clone, Serialize)]
567pub struct CompactionResult {
568    /// original count.
569    pub original_count: usize,
570    /// compacted count.
571    pub compacted_count: usize,
572    /// tokens saved.
573    pub tokens_saved: Option<usize>,
574}
575
576// ============================================================================
577// Event Types
578// ============================================================================
579
580/// Events emitted by the RPC server to the client during streaming.
581#[derive(Debug, Clone, Serialize)]
582#[serde(tag = "type", rename_all = "snake_case")]
583pub enum RpcEvent {
584    /// Agent started processing
585    AgentStart,
586    /// Streaming text chunk
587    TextChunk {
588        /// Streaming text content.
589        text: String,
590    },
591    /// Agent is thinking
592    Thinking,
593    /// Tool execution started
594    ToolStart {
595        /// Name of the tool being executed.
596        tool: String,
597    },
598    /// Tool execution finished
599    ToolEnd {
600        /// Name of the tool that finished.
601        tool: String,
602    },
603    /// Agent finished processing
604    AgentEnd,
605    /// Error occurred
606    Error {
607        /// Error message.
608        message: String,
609    },
610    /// Extension error
611    ExtensionError {
612        /// Filesystem path of the extension that errored.
613        extension_path: String,
614        /// Event name during which the error occurred.
615        event: String,
616        /// Error message.
617        error: String,
618    },
619}
620
621// ============================================================================
622// Pending Extension UI Request
623// ============================================================================
624
625/// Tracks pending extension UI requests awaiting client responses.
626pub struct PendingExtensionRequest {
627    /// Sender to deliver the client's response back to the awaiter.
628    pub resolve: tokio::sync::oneshot::Sender<RpcExtensionUiResponse>,
629}
630
631// ============================================================================
632// JSON-RPC 2.0 Method Mapping
633// ============================================================================
634
635/// Map a JSON-RPC 2.0 method + params to an internal RPC command value.
636pub fn jsonrpc_to_command(method: &str, params: Option<Value>, id: Option<Value>) -> Option<Value> {
637    let id_str = id.map(|v| match v {
638        Value::String(s) => s,
639        Value::Number(n) => n.to_string(),
640        _ => v.to_string(),
641    });
642
643    let cmd = match method {
644        "prompt" => serde_json::json!({
645            "type": "prompt",
646            "id": id_str,
647            "message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
648            "images": params.as_ref().and_then(|p| p.get("images")),
649            "streaming_behavior": params.as_ref().and_then(|p| p.get("streaming_behavior")),
650        }),
651        "steer" => serde_json::json!({
652            "type": "steer",
653            "id": id_str,
654            "message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
655        }),
656        "follow_up" => serde_json::json!({
657            "type": "follow_up",
658            "id": id_str,
659            "message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
660        }),
661        "abort" => serde_json::json!({ "type": "abort", "id": id_str }),
662        "new_session" => serde_json::json!({
663            "type": "new_session",
664            "id": id_str,
665            "parent_session": params.as_ref().and_then(|p| p.get("parent_session")).and_then(|v| v.as_str()),
666        }),
667        "get_state" => serde_json::json!({ "type": "get_state", "id": id_str }),
668        "set_model" => serde_json::json!({
669            "type": "set_model",
670            "id": id_str,
671            "provider": params.as_ref().and_then(|p| p.get("provider")).and_then(|v| v.as_str()).unwrap_or(""),
672            "model_id": params.as_ref().and_then(|p| p.get("modelId")).and_then(|v| v.as_str()).unwrap_or(""),
673        }),
674        "cycle_model" => serde_json::json!({ "type": "cycle_model", "id": id_str }),
675        "get_available_models" => {
676            serde_json::json!({ "type": "get_available_models", "id": id_str })
677        }
678        "set_thinking_level" => serde_json::json!({
679            "type": "set_thinking_level",
680            "id": id_str,
681            "level": params.as_ref().and_then(|p| p.get("level")).and_then(|v| v.as_str()).unwrap_or("default"),
682        }),
683        "cycle_thinking_level" => {
684            serde_json::json!({ "type": "cycle_thinking_level", "id": id_str })
685        }
686        "set_steering_mode" => serde_json::json!({
687            "type": "set_steering_mode",
688            "id": id_str,
689            "mode": params.as_ref().and_then(|p| p.get("mode")).and_then(|v| v.as_str()).unwrap_or("all"),
690        }),
691        "set_follow_up_mode" => serde_json::json!({
692            "type": "set_follow_up_mode",
693            "id": id_str,
694            "mode": params.as_ref().and_then(|p| p.get("mode")).and_then(|v| v.as_str()).unwrap_or("all"),
695        }),
696        "compact" => serde_json::json!({
697            "type": "compact",
698            "id": id_str,
699            "custom_instructions": params.as_ref().and_then(|p| p.get("customInstructions")).and_then(|v| v.as_str()),
700        }),
701        "set_auto_compaction" => serde_json::json!({
702            "type": "set_auto_compaction",
703            "id": id_str,
704            "enabled": params.as_ref().and_then(|p| p.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(true),
705        }),
706        "set_auto_retry" => serde_json::json!({
707            "type": "set_auto_retry",
708            "id": id_str,
709            "enabled": params.as_ref().and_then(|p| p.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(true),
710        }),
711        "abort_retry" => serde_json::json!({ "type": "abort_retry", "id": id_str }),
712        "bash" => serde_json::json!({
713            "type": "bash",
714            "id": id_str,
715            "command": params.as_ref().and_then(|p| p.get("command")).and_then(|v| v.as_str()).unwrap_or(""),
716        }),
717        "abort_bash" => serde_json::json!({ "type": "abort_bash", "id": id_str }),
718        "get_session_stats" => serde_json::json!({ "type": "get_session_stats", "id": id_str }),
719        "export_html" => serde_json::json!({
720            "type": "export_html",
721            "id": id_str,
722            "output_path": params.as_ref().and_then(|p| p.get("outputPath")).and_then(|v| v.as_str()),
723        }),
724        "switch_session" => serde_json::json!({
725            "type": "switch_session",
726            "id": id_str,
727            "session_path": params.as_ref().and_then(|p| p.get("sessionPath")).and_then(|v| v.as_str()).unwrap_or(""),
728        }),
729        "fork" => serde_json::json!({
730            "type": "fork",
731            "id": id_str,
732            "entry_id": params.as_ref().and_then(|p| p.get("entryId")).and_then(|v| v.as_str()).unwrap_or(""),
733        }),
734        "clone" => serde_json::json!({ "type": "clone", "id": id_str }),
735        "get_fork_messages" => serde_json::json!({ "type": "get_fork_messages", "id": id_str }),
736        "get_last_assistant_text" => {
737            serde_json::json!({ "type": "get_last_assistant_text", "id": id_str })
738        }
739        "set_session_name" => serde_json::json!({
740            "type": "set_session_name",
741            "id": id_str,
742            "name": params.as_ref().and_then(|p| p.get("name")).and_then(|v| v.as_str()).unwrap_or(""),
743        }),
744        "get_messages" => serde_json::json!({ "type": "get_messages", "id": id_str }),
745        "get_commands" => serde_json::json!({ "type": "get_commands", "id": id_str }),
746        _ => return None,
747    };
748
749    Some(cmd)
750}
751
752/// Convert an internal RpcResponse to a JSON-RPC 2.0 response.
753pub fn rpc_response_to_jsonrpc(response: &RpcResponse, rpc_id: Value) -> String {
754    match response {
755        RpcResponse::Response {
756            success: true,
757            data,
758            ..
759        } => {
760            let result = data.clone().unwrap_or(Value::Null);
761            let resp = JsonRpcSuccessResponse {
762                jsonrpc: "2.0".to_string(),
763                id: rpc_id,
764                result,
765            };
766            serde_json::to_string(&resp).unwrap_or_default()
767        }
768        RpcResponse::Response {
769            success: false,
770            error,
771            ..
772        } => {
773            let resp = JsonRpcErrorResponse {
774                jsonrpc: "2.0".to_string(),
775                id: rpc_id,
776                error: JsonRpcError {
777                    code: JSONRPC_INTERNAL_ERROR,
778                    message: error.clone().unwrap_or_default(),
779                    data: None,
780                },
781            };
782            serde_json::to_string(&resp).unwrap_or_default()
783        }
784        RpcResponse::ExtensionUiRequest(_) => {
785            // Extension UI requests don't map cleanly to JSON-RPC
786            // In JSON-RPC mode we skip these or send as notification
787            String::new()
788        }
789    }
790}
791
792// ============================================================================
793// JSONL Line Reader
794// ============================================================================
795
796/// A strict JSONL line reader that splits on LF only (not CR or other separators).
797/// This is important because JSON strings may contain U+2028/U+2029 which
798/// Node.js readline would incorrectly split on.
799pub struct JsonlLineReader {
800    buffer: String,
801}
802
803impl JsonlLineReader {
804    /// Create a new JSONL line reader.
805    pub fn new() -> Self {
806        Self {
807            buffer: String::new(),
808        }
809    }
810
811    /// Feed data into the reader, returning complete lines.
812    /// Lines are terminated by `\n` only. Trailing `\r` is stripped.
813    pub fn feed(&mut self, data: &str) -> Vec<String> {
814        self.buffer.push_str(data);
815        let mut lines = Vec::new();
816
817        while let Some(pos) = self.buffer.find('\n') {
818            let line = self.buffer[..pos].to_string();
819            self.buffer = self.buffer[pos + 1..].to_string();
820            // Strip trailing CR
821            let trimmed = line.trim_end_matches('\r').to_string();
822            if !trimmed.is_empty() {
823                lines.push(trimmed);
824            }
825        }
826
827        lines
828    }
829
830    /// Flush any remaining data as a final line (for EOF handling).
831    pub fn flush(&mut self) -> Option<String> {
832        if self.buffer.is_empty() {
833            return None;
834        }
835        let line = self.buffer.trim_end_matches('\r').to_string();
836        self.buffer.clear();
837        if line.is_empty() { None } else { Some(line) }
838    }
839
840    /// Check if there's buffered data.
841    pub fn has_buffered_data(&self) -> bool {
842        !self.buffer.is_empty()
843    }
844}
845
846impl Default for JsonlLineReader {
847    fn default() -> Self {
848        Self::new()
849    }
850}
851
852// ============================================================================
853// Output Writer
854// ============================================================================
855
856/// Thread-safe output writer that ensures atomic JSONL writes.
857pub struct RpcOutput {
858    inner: Arc<parking_lot::Mutex<std::io::Stdout>>,
859}
860
861impl RpcOutput {
862    /// Create a new output writer wrapping stdout.
863    pub fn new() -> Self {
864        Self {
865            inner: Arc::new(parking_lot::Mutex::new(std::io::stdout())),
866        }
867    }
868
869    /// Write a JSONL record atomically.
870    pub fn write_line(&self, value: &Value) {
871        let line = serialize_json_line(value);
872        let mut out = self.inner.lock();
873        let _ = out.write_all(line.as_bytes());
874        let _ = out.flush();
875    }
876
877    /// Write a serializable value as a JSONL record.
878    pub fn write_obj<T: Serialize>(&self, value: &T) {
879        let line = serialize_json_line_obj(value);
880        let mut out = self.inner.lock();
881        let _ = out.write_all(line.as_bytes());
882        let _ = out.flush();
883    }
884
885    /// Write a raw string (must already be a complete JSONL record with trailing LF).
886    pub fn write_raw(&self, raw: &str) {
887        let mut out = self.inner.lock();
888        let _ = out.write_all(raw.as_bytes());
889        let _ = out.flush();
890    }
891}
892
893impl Default for RpcOutput {
894    fn default() -> Self {
895        Self::new()
896    }
897}
898
899impl Clone for RpcOutput {
900    fn clone(&self) -> Self {
901        Self {
902            inner: Arc::clone(&self.inner),
903        }
904    }
905}
906
907// ============================================================================
908// Session Handoff Protocol
909// ============================================================================
910
911/// Information about a session for handoff between processes.
912#[derive(Debug, Clone, Serialize, Deserialize)]
913pub struct SessionHandoff {
914    /// Unique session identifier
915    pub session_id: String,
916    /// Session file path
917    pub session_file: Option<String>,
918    /// Session display name
919    pub session_name: Option<String>,
920    /// Parent session ID (for forked sessions)
921    pub parent_session_id: Option<String>,
922    /// Model being used
923    pub model_id: Option<String>,
924    /// Thinking level
925    pub thinking_level: Option<String>,
926    /// Message count at handoff time
927    pub message_count: usize,
928    /// Timestamp of handoff
929    pub timestamp: i64,
930}
931
932impl SessionHandoff {
933    /// Create a handoff from current session state.
934    pub fn from_state(state: &SessionState) -> Self {
935        Self {
936            session_id: state.session_id.clone(),
937            session_file: state.session_file.clone(),
938            session_name: state.session_name.clone(),
939            parent_session_id: None,
940            model_id: state
941                .model
942                .as_ref()
943                .map(|m| format!("{}/{}", m.provider, m.id)),
944            thinking_level: Some(state.thinking_level.clone()),
945            message_count: state.message_count,
946            timestamp: chrono::Utc::now().timestamp(),
947        }
948    }
949
950    /// Serialize to JSON for transmission.
951    pub fn to_json(&self) -> Result<String> {
952        Ok(serde_json::to_string(self)?)
953    }
954
955    /// Deserialize from JSON.
956    pub fn from_json(json: &str) -> Result<Self> {
957        Ok(serde_json::from_str(json)?)
958    }
959}