use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::Write;
use std::sync::Arc;
pub fn serialize_json_line(value: &Value) -> String {
format!("{}\n", serde_json::to_string(value).unwrap_or_default())
}
pub fn serialize_json_line_obj<T: Serialize>(value: &T) -> String {
match serde_json::to_string(value) {
Ok(s) => format!("{}\n", s),
Err(e) => {
tracing::error!("Failed to serialize JSONL: {}", e);
"{\"type\":\"response\",\"command\":\"internal\",\"success\":false,\"error\":\"Serialization error\"}\n".to_string()
}
}
}
pub fn parse_json_line(line: &str) -> Result<Value, serde_json::Error> {
let trimmed = line.trim_end_matches('\r');
serde_json::from_str(trimmed)
}
#[derive(Debug, Clone, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String, #[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonRpcSuccessResponse {
pub jsonrpc: String,
pub id: Value,
pub result: Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonRpcErrorResponse {
pub jsonrpc: String,
pub id: Value,
pub error: JsonRpcError,
}
pub const JSONRPC_PARSE_ERROR: i64 = -32700;
pub const JSONRPC_INVALID_REQUEST: i64 = -32600;
pub const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
pub const JSONRPC_INVALID_PARAMS: i64 = -32602;
pub const JSONRPC_INTERNAL_ERROR: i64 = -32603;
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RpcCommand {
Prompt {
id: Option<String>,
message: String,
images: Option<Vec<ImageData>>,
#[serde(default)]
streaming_behavior: Option<String>,
},
Steer {
id: Option<String>,
message: String,
images: Option<Vec<ImageData>>,
},
FollowUp {
id: Option<String>,
message: String,
images: Option<Vec<ImageData>>,
},
Abort {
id: Option<String>,
},
NewSession {
id: Option<String>,
parent_session: Option<String>,
},
GetState {
id: Option<String>,
},
SetModel {
id: Option<String>,
provider: String,
model_id: String,
},
CycleModel {
id: Option<String>,
},
GetAvailableModels {
id: Option<String>,
},
SetThinkingLevel {
id: Option<String>,
level: String,
},
CycleThinkingLevel {
id: Option<String>,
},
SetSteeringMode {
id: Option<String>,
mode: String,
},
SetFollowUpMode {
id: Option<String>,
mode: String,
},
Compact {
id: Option<String>,
custom_instructions: Option<String>,
},
SetAutoCompaction {
id: Option<String>,
enabled: bool,
},
SetAutoRetry {
id: Option<String>,
enabled: bool,
},
AbortRetry {
id: Option<String>,
},
Bash {
id: Option<String>,
command: String,
},
AbortBash {
id: Option<String>,
},
GetSessionStats {
id: Option<String>,
},
ExportHtml {
id: Option<String>,
output_path: Option<String>,
},
SwitchSession {
id: Option<String>,
session_path: String,
},
Fork {
id: Option<String>,
entry_id: String,
},
Clone {
id: Option<String>,
},
GetForkMessages {
id: Option<String>,
},
GetLastAssistantText {
id: Option<String>,
},
SetSessionName {
id: Option<String>,
name: String,
},
GetMessages {
id: Option<String>,
},
GetCommands {
id: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageData {
pub source: String,
#[serde(rename = "type")]
pub media_type: String,
}
#[derive(Debug, Clone)]
pub struct RpcImageSource {
pub data: Vec<u8>,
pub mime_type: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RpcResponse {
Response {
id: Option<String>,
command: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
ExtensionUiRequest(RpcExtensionUiRequest),
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum RpcExtensionUiRequest {
Select {
id: String,
title: String,
options: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
},
Confirm {
id: String,
title: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
},
Input {
id: String,
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
},
Editor {
id: String,
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
prefill: Option<String>,
},
Notify {
id: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
notify_type: Option<String>,
},
SetStatus {
id: String,
status_key: String,
status_text: Option<String>,
},
SetWidget {
id: String,
widget_key: String,
widget_lines: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
widget_placement: Option<String>,
},
SetTitle {
id: String,
title: String,
},
SetEditorText {
id: String,
text: String,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RpcExtensionUiResponse {
ExtensionUiResponse {
id: String,
#[serde(default)]
value: Option<String>,
#[serde(default)]
confirmed: Option<bool>,
#[serde(default)]
cancelled: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct SessionState {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<ModelInfo>,
pub thinking_level: String,
pub is_streaming: bool,
pub is_compacting: bool,
pub steering_mode: String,
pub follow_up_mode: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_file: Option<String>,
pub session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_name: Option<String>,
pub auto_compaction_enabled: bool,
pub message_count: usize,
pub pending_message_count: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelInfo {
pub provider: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct CommandInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_info: Option<SourceInfo>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SourceInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SessionStats {
pub message_count: usize,
pub token_count: Option<usize>,
pub last_activity: Option<i64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompactionResult {
pub original_count: usize,
pub compacted_count: usize,
pub tokens_saved: Option<usize>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RpcEvent {
AgentStart,
TextChunk {
text: String,
},
Thinking,
ToolStart {
tool: String,
},
ToolEnd {
tool: String,
},
AgentEnd,
Error {
message: String,
},
ExtensionError {
extension_path: String,
event: String,
error: String,
},
}
pub struct PendingExtensionRequest {
pub resolve: tokio::sync::oneshot::Sender<RpcExtensionUiResponse>,
}
pub fn jsonrpc_to_command(method: &str, params: Option<Value>, id: Option<Value>) -> Option<Value> {
let id_str = id.map(|v| match v {
Value::String(s) => s,
Value::Number(n) => n.to_string(),
_ => v.to_string(),
});
let cmd = match method {
"prompt" => serde_json::json!({
"type": "prompt",
"id": id_str,
"message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
"images": params.as_ref().and_then(|p| p.get("images")),
"streaming_behavior": params.as_ref().and_then(|p| p.get("streaming_behavior")),
}),
"steer" => serde_json::json!({
"type": "steer",
"id": id_str,
"message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
}),
"follow_up" => serde_json::json!({
"type": "follow_up",
"id": id_str,
"message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
}),
"abort" => serde_json::json!({ "type": "abort", "id": id_str }),
"new_session" => serde_json::json!({
"type": "new_session",
"id": id_str,
"parent_session": params.as_ref().and_then(|p| p.get("parent_session")).and_then(|v| v.as_str()),
}),
"get_state" => serde_json::json!({ "type": "get_state", "id": id_str }),
"set_model" => serde_json::json!({
"type": "set_model",
"id": id_str,
"provider": params.as_ref().and_then(|p| p.get("provider")).and_then(|v| v.as_str()).unwrap_or(""),
"model_id": params.as_ref().and_then(|p| p.get("modelId")).and_then(|v| v.as_str()).unwrap_or(""),
}),
"cycle_model" => serde_json::json!({ "type": "cycle_model", "id": id_str }),
"get_available_models" => {
serde_json::json!({ "type": "get_available_models", "id": id_str })
}
"set_thinking_level" => serde_json::json!({
"type": "set_thinking_level",
"id": id_str,
"level": params.as_ref().and_then(|p| p.get("level")).and_then(|v| v.as_str()).unwrap_or("default"),
}),
"cycle_thinking_level" => {
serde_json::json!({ "type": "cycle_thinking_level", "id": id_str })
}
"set_steering_mode" => serde_json::json!({
"type": "set_steering_mode",
"id": id_str,
"mode": params.as_ref().and_then(|p| p.get("mode")).and_then(|v| v.as_str()).unwrap_or("all"),
}),
"set_follow_up_mode" => serde_json::json!({
"type": "set_follow_up_mode",
"id": id_str,
"mode": params.as_ref().and_then(|p| p.get("mode")).and_then(|v| v.as_str()).unwrap_or("all"),
}),
"compact" => serde_json::json!({
"type": "compact",
"id": id_str,
"custom_instructions": params.as_ref().and_then(|p| p.get("customInstructions")).and_then(|v| v.as_str()),
}),
"set_auto_compaction" => serde_json::json!({
"type": "set_auto_compaction",
"id": id_str,
"enabled": params.as_ref().and_then(|p| p.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(true),
}),
"set_auto_retry" => serde_json::json!({
"type": "set_auto_retry",
"id": id_str,
"enabled": params.as_ref().and_then(|p| p.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(true),
}),
"abort_retry" => serde_json::json!({ "type": "abort_retry", "id": id_str }),
"bash" => serde_json::json!({
"type": "bash",
"id": id_str,
"command": params.as_ref().and_then(|p| p.get("command")).and_then(|v| v.as_str()).unwrap_or(""),
}),
"abort_bash" => serde_json::json!({ "type": "abort_bash", "id": id_str }),
"get_session_stats" => serde_json::json!({ "type": "get_session_stats", "id": id_str }),
"export_html" => serde_json::json!({
"type": "export_html",
"id": id_str,
"output_path": params.as_ref().and_then(|p| p.get("outputPath")).and_then(|v| v.as_str()),
}),
"switch_session" => serde_json::json!({
"type": "switch_session",
"id": id_str,
"session_path": params.as_ref().and_then(|p| p.get("sessionPath")).and_then(|v| v.as_str()).unwrap_or(""),
}),
"fork" => serde_json::json!({
"type": "fork",
"id": id_str,
"entry_id": params.as_ref().and_then(|p| p.get("entryId")).and_then(|v| v.as_str()).unwrap_or(""),
}),
"clone" => serde_json::json!({ "type": "clone", "id": id_str }),
"get_fork_messages" => serde_json::json!({ "type": "get_fork_messages", "id": id_str }),
"get_last_assistant_text" => {
serde_json::json!({ "type": "get_last_assistant_text", "id": id_str })
}
"set_session_name" => serde_json::json!({
"type": "set_session_name",
"id": id_str,
"name": params.as_ref().and_then(|p| p.get("name")).and_then(|v| v.as_str()).unwrap_or(""),
}),
"get_messages" => serde_json::json!({ "type": "get_messages", "id": id_str }),
"get_commands" => serde_json::json!({ "type": "get_commands", "id": id_str }),
_ => return None,
};
Some(cmd)
}
pub fn rpc_response_to_jsonrpc(response: &RpcResponse, rpc_id: Value) -> String {
match response {
RpcResponse::Response {
success: true,
data,
..
} => {
let result = data.clone().unwrap_or(Value::Null);
let resp = JsonRpcSuccessResponse {
jsonrpc: "2.0".to_string(),
id: rpc_id,
result,
};
serde_json::to_string(&resp).unwrap_or_default()
}
RpcResponse::Response {
success: false,
error,
..
} => {
let resp = JsonRpcErrorResponse {
jsonrpc: "2.0".to_string(),
id: rpc_id,
error: JsonRpcError {
code: JSONRPC_INTERNAL_ERROR,
message: error.clone().unwrap_or_default(),
data: None,
},
};
serde_json::to_string(&resp).unwrap_or_default()
}
RpcResponse::ExtensionUiRequest(_) => {
String::new()
}
}
}
pub struct JsonlLineReader {
buffer: String,
}
impl JsonlLineReader {
pub fn new() -> Self {
Self {
buffer: String::new(),
}
}
pub fn feed(&mut self, data: &str) -> Vec<String> {
self.buffer.push_str(data);
let mut lines = Vec::new();
while let Some(pos) = self.buffer.find('\n') {
let line = self.buffer[..pos].to_string();
self.buffer = self.buffer[pos + 1..].to_string();
let trimmed = line.trim_end_matches('\r').to_string();
if !trimmed.is_empty() {
lines.push(trimmed);
}
}
lines
}
pub fn flush(&mut self) -> Option<String> {
if self.buffer.is_empty() {
return None;
}
let line = self.buffer.trim_end_matches('\r').to_string();
self.buffer.clear();
if line.is_empty() {
None
} else {
Some(line)
}
}
pub fn has_buffered_data(&self) -> bool {
!self.buffer.is_empty()
}
}
impl Default for JsonlLineReader {
fn default() -> Self {
Self::new()
}
}
pub struct RpcOutput {
inner: Arc<parking_lot::Mutex<std::io::Stdout>>,
}
impl RpcOutput {
pub fn new() -> Self {
Self {
inner: Arc::new(parking_lot::Mutex::new(std::io::stdout())),
}
}
pub fn write_line(&self, value: &Value) {
let line = serialize_json_line(value);
let mut out = self.inner.lock();
let _ = out.write_all(line.as_bytes());
let _ = out.flush();
}
pub fn write_obj<T: Serialize>(&self, value: &T) {
let line = serialize_json_line_obj(value);
let mut out = self.inner.lock();
let _ = out.write_all(line.as_bytes());
let _ = out.flush();
}
pub fn write_raw(&self, raw: &str) {
let mut out = self.inner.lock();
let _ = out.write_all(raw.as_bytes());
let _ = out.flush();
}
}
impl Default for RpcOutput {
fn default() -> Self {
Self::new()
}
}
impl Clone for RpcOutput {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionHandoff {
pub session_id: String,
pub session_file: Option<String>,
pub session_name: Option<String>,
pub parent_session_id: Option<String>,
pub model_id: Option<String>,
pub thinking_level: Option<String>,
pub message_count: usize,
pub timestamp: i64,
}
impl SessionHandoff {
pub fn from_state(state: &SessionState) -> Self {
Self {
session_id: state.session_id.clone(),
session_file: state.session_file.clone(),
session_name: state.session_name.clone(),
parent_session_id: None,
model_id: state
.model
.as_ref()
.map(|m| format!("{}/{}", m.provider, m.id)),
thinking_level: Some(state.thinking_level.clone()),
message_count: state.message_count,
timestamp: chrono::Utc::now().timestamp(),
}
}
pub fn to_json(&self) -> Result<String> {
Ok(serde_json::to_string(self)?)
}
pub fn from_json(json: &str) -> Result<Self> {
Ok(serde_json::from_str(json)?)
}
}