#![allow(unused_imports)]
use crate::App;
use anyhow::{Context, Result};
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::{BufRead, Write};
use std::sync::{Arc, RwLock};
use tokio::sync::{mpsc, oneshot};
use crate::interactive::InteractiveState;
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RpcCommand {
Prompt {
id: Option<String>,
message: String,
images: Option<Vec<ImageData>>,
},
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 {
pub thinking_level: String,
pub is_streaming: bool,
pub is_compacting: bool,
pub steering_mode: String,
pub follow_up_mode: 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 CommandInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub source: 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>,
}
pub struct RpcServer {
port: u16,
shutdown: Arc<RwLock<bool>>,
session_state: Arc<RwLock<SessionState>>,
}
impl RpcServer {
pub fn new(port: u16) -> Self {
Self {
port,
shutdown: Arc::new(RwLock::new(false)),
session_state: Arc::new(RwLock::new(SessionState {
thinking_level: "default".to_string(),
is_streaming: false,
is_compacting: false,
steering_mode: "all".to_string(),
follow_up_mode: "all".to_string(),
session_id: uuid::Uuid::new_v4().to_string(),
session_name: None,
auto_compaction_enabled: true,
message_count: 0,
pending_message_count: 0,
})),
}
}
pub fn port(&self) -> u16 {
self.port
}
pub fn is_shutdown_requested(&self) -> bool {
*self.shutdown.read().unwrap()
}
pub fn request_shutdown(&self) {
*self.shutdown.write().unwrap() = true;
}
pub fn update_session_state<F>(&self, f: F)
where
F: FnOnce(&mut SessionState),
{
let mut state = self.session_state.write().unwrap();
f(&mut state);
}
pub fn get_session_state(&self) -> SessionState {
self.session_state.read().unwrap().clone()
}
pub fn parse_images(images: Option<Vec<ImageData>>) -> Vec<RpcImageSource> {
images
.unwrap_or_default()
.into_iter()
.filter_map(|img| {
if img.source.starts_with("data:") {
let parts: Vec<&str> = img.source.splitn(2, ',').collect();
if parts.len() == 2 {
let header = parts[0];
let data = parts[1];
let base64_data = data.split(';').next().unwrap_or(data);
if let Ok(decoded) =
base64::engine::general_purpose::STANDARD.decode(base64_data)
{
return Some(RpcImageSource {
data: decoded,
mime_type: img.media_type,
});
}
}
}
None
})
.collect()
}
}
pub async fn run_rpc_mode(app: App) -> Result<()> {
let server = Arc::new(RpcServer::new(0));
let stdin = std::io::stdin();
let mut input = stdin.lock();
let mut line = String::new();
loop {
if server.is_shutdown_requested() {
break;
}
line.clear();
match input.read_line(&mut line) {
Ok(0) => {
break;
}
Ok(_) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(value) => {
let response = handle_command(&server, &app, value);
if let Some(response) = response {
let json = serde_json::to_string(&response)
.context("Failed to serialize response")?;
println!("{}", json);
}
}
Err(e) => {
let error_response = RpcResponse::Response {
id: None,
command: "parse".to_string(),
success: false,
data: None,
error: Some(format!("Parse error: {}", e)),
};
let json = serde_json::to_string(&error_response)
.context("Failed to serialize error response")?;
println!("{}", json);
}
}
}
Err(e) => {
eprintln!("Error reading stdin: {}", e);
break;
}
}
}
Ok(())
}
fn handle_command(
server: &Arc<RpcServer>,
app: &App,
value: serde_json::Value,
) -> Option<RpcResponse> {
if let Some(obj) = value.as_object() {
if obj.get("type").and_then(|v| v.as_str()) == Some("extension_ui_response") {
return None;
}
}
let command: RpcCommand = match serde_json::from_value(value) {
Ok(cmd) => cmd,
Err(e) => {
return Some(RpcResponse::Response {
id: None,
command: "parse".to_string(),
success: false,
data: None,
error: Some(format!("Parse error: {}", e)),
});
}
};
Some(execute_command(server, app, command))
}
fn execute_command(server: &Arc<RpcServer>, app: &App, command: RpcCommand) -> RpcResponse {
match command {
RpcCommand::Prompt {
id,
message,
images,
} => {
let image_sources = RpcServer::parse_images(images);
server.update_session_state(|s| {
s.is_streaming = true;
s.pending_message_count += 1;
});
RpcResponse::Response {
id,
command: "prompt".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::Steer {
id,
message,
images,
} => {
server.update_session_state(|s| {
s.steering_mode = "one_at_a_time".to_string();
});
RpcResponse::Response {
id,
command: "steer".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::FollowUp {
id,
message,
images,
} => {
server.update_session_state(|s| {
s.follow_up_mode = "one_at_a_time".to_string();
});
RpcResponse::Response {
id,
command: "follow_up".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::Abort { id } => {
server.update_session_state(|s| {
s.is_streaming = false;
});
RpcResponse::Response {
id,
command: "abort".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::NewSession {
id,
parent_session: _,
} => {
server.update_session_state(|s| {
s.session_id = uuid::Uuid::new_v4().to_string();
s.message_count = 0;
s.pending_message_count = 0;
});
RpcResponse::Response {
id,
command: "new_session".to_string(),
success: true,
data: Some(serde_json::json!({ "cancelled": false })),
error: None,
}
}
RpcCommand::GetState { id } => {
let state = server.get_session_state();
RpcResponse::Response {
id,
command: "get_state".to_string(),
success: true,
data: Some(serde_json::to_value(&state).unwrap()),
error: None,
}
}
RpcCommand::SetModel {
id,
provider,
model_id,
} => {
RpcResponse::Response {
id,
command: "set_model".to_string(),
success: true,
data: Some(serde_json::json!({
"provider": provider,
"id": model_id
})),
error: None,
}
}
RpcCommand::CycleModel { id } => RpcResponse::Response {
id,
command: "cycle_model".to_string(),
success: true,
data: Some(serde_json::json!({
"model": null,
"thinking_level": "default",
"is_scoped": false
})),
error: None,
},
RpcCommand::GetAvailableModels { id } => RpcResponse::Response {
id,
command: "get_available_models".to_string(),
success: true,
data: Some(serde_json::json!({
"models": []
})),
error: None,
},
RpcCommand::SetThinkingLevel { id, level } => {
server.update_session_state(|s| {
s.thinking_level = level;
});
RpcResponse::Response {
id,
command: "set_thinking_level".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::CycleThinkingLevel { id } => {
let current = server.get_session_state().thinking_level;
let next = match current.as_str() {
"off" => "default",
"default" => "medium",
"medium" => "high",
_ => "off",
};
server.update_session_state(|s| {
s.thinking_level = next.to_string();
});
RpcResponse::Response {
id,
command: "cycle_thinking_level".to_string(),
success: true,
data: Some(serde_json::json!({ "level": next })),
error: None,
}
}
RpcCommand::SetSteeringMode { id, mode } => {
server.update_session_state(|s| {
s.steering_mode = mode;
});
RpcResponse::Response {
id,
command: "set_steering_mode".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::SetFollowUpMode { id, mode } => {
server.update_session_state(|s| {
s.follow_up_mode = mode;
});
RpcResponse::Response {
id,
command: "set_follow_up_mode".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::Compact {
id,
custom_instructions: _,
} => {
server.update_session_state(|s| {
s.is_compacting = true;
});
let state = server.get_session_state();
let result = CompactionResult {
original_count: state.message_count,
compacted_count: (state.message_count as f32 * 0.7) as usize,
tokens_saved: Some(1000),
};
server.update_session_state(|s| {
s.is_compacting = false;
s.message_count = result.compacted_count;
});
RpcResponse::Response {
id,
command: "compact".to_string(),
success: true,
data: Some(serde_json::to_value(&result).unwrap()),
error: None,
}
}
RpcCommand::SetAutoCompaction { id, enabled } => {
server.update_session_state(|s| {
s.auto_compaction_enabled = enabled;
});
RpcResponse::Response {
id,
command: "set_auto_compaction".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::SetAutoRetry { id, enabled: _ } => RpcResponse::Response {
id,
command: "set_auto_retry".to_string(),
success: true,
data: None,
error: None,
},
RpcCommand::AbortRetry { id } => RpcResponse::Response {
id,
command: "abort_retry".to_string(),
success: true,
data: None,
error: None,
},
RpcCommand::Bash { id, command } => {
let output = std::process::Command::new("sh")
.arg("-c")
.arg(&command)
.output();
match output {
Ok(output) => RpcResponse::Response {
id,
command: "bash".to_string(),
success: true,
data: Some(serde_json::json!({
"stdout": String::from_utf8_lossy(&output.stdout),
"stderr": String::from_utf8_lossy(&output.stderr),
"exit_code": output.status.code()
})),
error: None,
},
Err(e) => RpcResponse::Response {
id,
command: "bash".to_string(),
success: false,
data: None,
error: Some(e.to_string()),
},
}
}
RpcCommand::AbortBash { id } => RpcResponse::Response {
id,
command: "abort_bash".to_string(),
success: true,
data: None,
error: None,
},
RpcCommand::GetSessionStats { id } => {
let state = server.get_session_state();
let stats = SessionStats {
message_count: state.message_count,
token_count: None,
last_activity: None,
};
RpcResponse::Response {
id,
command: "get_session_stats".to_string(),
success: true,
data: Some(serde_json::to_value(&stats).unwrap()),
error: None,
}
}
RpcCommand::ExportHtml { id, output_path: _ } => RpcResponse::Response {
id,
command: "export_html".to_string(),
success: true,
data: Some(serde_json::json!({ "path": "session.html" })),
error: None,
},
RpcCommand::SwitchSession {
id,
session_path: _,
} => {
server.update_session_state(|s| {
s.session_id = uuid::Uuid::new_v4().to_string();
s.message_count = 0;
});
RpcResponse::Response {
id,
command: "switch_session".to_string(),
success: true,
data: Some(serde_json::json!({ "cancelled": false })),
error: None,
}
}
RpcCommand::Fork { id, entry_id: _ } => RpcResponse::Response {
id,
command: "fork".to_string(),
success: true,
data: Some(serde_json::json!({
"text": "",
"cancelled": false
})),
error: None,
},
RpcCommand::Clone { id } => RpcResponse::Response {
id,
command: "clone".to_string(),
success: true,
data: Some(serde_json::json!({ "cancelled": false })),
error: None,
},
RpcCommand::GetForkMessages { id } => RpcResponse::Response {
id,
command: "get_fork_messages".to_string(),
success: true,
data: Some(serde_json::json!({
"messages": []
})),
error: None,
},
RpcCommand::GetLastAssistantText { id } => RpcResponse::Response {
id,
command: "get_last_assistant_text".to_string(),
success: true,
data: Some(serde_json::json!({
"text": null
})),
error: None,
},
RpcCommand::SetSessionName { id, name } => {
server.update_session_state(|s| {
s.session_name = if name.is_empty() { None } else { Some(name) };
});
RpcResponse::Response {
id,
command: "set_session_name".to_string(),
success: true,
data: None,
error: None,
}
}
RpcCommand::GetMessages { id } => RpcResponse::Response {
id,
command: "get_messages".to_string(),
success: true,
data: Some(serde_json::json!({
"messages": []
})),
error: None,
},
RpcCommand::GetCommands { id } => {
let commands = vec![
CommandInfo {
name: "compact".to_string(),
description: Some("Compact context".to_string()),
source: "builtin".to_string(),
},
CommandInfo {
name: "clear".to_string(),
description: Some("Clear conversation".to_string()),
source: "builtin".to_string(),
},
];
RpcResponse::Response {
id,
command: "get_commands".to_string(),
success: true,
data: Some(serde_json::to_value(&commands).unwrap()),
error: None,
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PasteState {
Normal,
Pasting,
}
pub struct PasteHandler {
state: PasteState,
buffer: Vec<u8>,
start_sequence: Vec<u8>,
end_sequence: Vec<u8>,
}
impl PasteHandler {
pub fn new() -> Self {
Self {
state: PasteState::Normal,
buffer: Vec::new(),
start_sequence: vec![0x1B, 0x5B, 0x32, 0x30, 0x30, 0x7E], end_sequence: vec![0x1B, 0x5B, 0x32, 0x30, 0x31, 0x7E], }
}
pub fn reset(&mut self) {
self.state = PasteState::Normal;
self.buffer.clear();
}
pub fn state(&self) -> PasteState {
self.state.clone()
}
pub fn buffer(&self) -> &[u8] {
&self.buffer
}
pub fn process_byte(&mut self, byte: u8) -> Option<u8> {
match self.state {
PasteState::Normal => {
if self.buffer.is_empty() && byte == 0x1B {
self.buffer.push(byte);
None
} else if self.buffer.len() >= 1 && self.buffer[0] == 0x1B && byte == 0x5B {
self.buffer.push(byte);
None
} else if self.buffer.len() >= 2
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& byte == 0x32
{
self.buffer.push(byte);
None
} else if self.buffer.len() >= 3
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& self.buffer[2] == 0x32
&& byte == 0x30
{
self.buffer.push(byte);
None
} else if self.buffer.len() >= 4
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& self.buffer[2] == 0x32
&& self.buffer[3] == 0x30
&& byte == 0x30
{
self.buffer.push(byte);
None
} else if self.buffer.len() >= 5
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& self.buffer[2] == 0x32
&& self.buffer[3] == 0x30
&& self.buffer[4] == 0x30
&& byte == 0x7E
{
self.buffer.clear();
self.state = PasteState::Pasting;
None
} else {
let first_byte = self.buffer.first().copied();
self.buffer.clear();
first_byte
}
}
PasteState::Pasting => {
if self.buffer.is_empty() && byte == 0x1B {
self.buffer.push(byte);
None
} else if self.buffer.len() >= 1 && self.buffer[0] == 0x1B && byte == 0x5B {
self.buffer.push(byte);
None
} else if self.buffer.len() >= 2
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& byte == 0x32
{
self.buffer.push(byte);
None
} else if self.buffer.len() >= 3
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& self.buffer[2] == 0x32
&& byte == 0x30
{
self.buffer.push(byte);
None
} else if self.buffer.len() >= 4
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& self.buffer[2] == 0x32
&& self.buffer[3] == 0x30
&& byte == 0x31
{
self.buffer.push(byte);
None
} else if self.buffer.len() >= 5
&& self.buffer[0] == 0x1B
&& self.buffer[1] == 0x5B
&& self.buffer[2] == 0x32
&& self.buffer[3] == 0x30
&& self.buffer[4] == 0x31
&& byte == 0x7E
{
self.buffer.clear();
self.state = PasteState::Normal;
None
} else {
if !self.buffer.is_empty() {
}
self.buffer.push(byte);
None
}
}
}
}
pub fn ends_with(&self, sequence: &[u8]) -> bool {
if self.buffer.len() < sequence.len() {
return false;
}
let end_pos = self.buffer.len() - sequence.len();
&self.buffer[end_pos..] == sequence
}
pub fn extract_image_data(&self) -> Option<Vec<u8>> {
let buffer = self.buffer();
if buffer.len() < 8 {
return None;
}
if buffer.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return Some(buffer.to_vec());
}
if buffer.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some(buffer.to_vec());
}
if buffer.iter().take(100).filter(|&&b| b == 0).count() > 5 {
return Some(buffer.to_vec());
}
None
}
}
impl Default for PasteHandler {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rpc_server_new() {
let server = RpcServer::new(8080);
assert_eq!(server.port(), 8080);
assert!(!server.is_shutdown_requested());
}
#[test]
fn test_rpc_server_shutdown() {
let server = Arc::new(RpcServer::new(0));
assert!(!server.is_shutdown_requested());
server.request_shutdown();
assert!(server.is_shutdown_requested());
}
#[test]
fn test_rpc_server_session_state() {
let server = Arc::new(RpcServer::new(0));
let state = server.get_session_state();
assert_eq!(state.message_count, 0);
assert_eq!(state.thinking_level, "default");
assert!(state.auto_compaction_enabled);
server.update_session_state(|s| {
s.message_count = 10;
s.thinking_level = "high".to_string();
});
let new_state = server.get_session_state();
assert_eq!(new_state.message_count, 10);
assert_eq!(new_state.thinking_level, "high");
}
#[test]
fn test_parse_images_data_uri() {
let images = vec![ImageData {
source: "data:image/png;base64,iVBORw0KGgo=".to_string(),
media_type: "image/png".to_string(),
}];
let parsed = RpcServer::parse_images(Some(images));
assert_eq!(parsed.len(), 1);
assert!(!parsed[0].data.is_empty());
assert_eq!(parsed[0].mime_type, "image/png");
}
#[test]
fn test_parse_images_empty() {
let parsed = RpcServer::parse_images(None);
assert!(parsed.is_empty());
}
#[test]
fn test_parse_images_non_data_uri() {
let images = vec![ImageData {
source: "https://example.com/image.png".to_string(),
media_type: "image/png".to_string(),
}];
let parsed = RpcServer::parse_images(Some(images));
assert!(parsed.is_empty());
}
#[test]
fn test_paste_handler_initial_state() {
let handler = PasteHandler::new();
assert_eq!(handler.state(), PasteState::Normal);
assert!(handler.buffer().is_empty());
}
#[test]
fn test_paste_handler_reset() {
let mut handler = PasteHandler::new();
handler.buffer.push(b't');
handler.buffer.push(b'e');
handler.buffer.push(b's');
handler.state = PasteState::Pasting;
handler.reset();
assert_eq!(handler.state(), PasteState::Normal);
assert!(handler.buffer().is_empty());
}
#[test]
fn test_paste_handler_paste_start_sequence() {
let mut handler = PasteHandler::new();
let result = handler.process_byte(0x1B);
assert!(result.is_none());
assert_eq!(handler.state(), PasteState::Normal);
let result = handler.process_byte(0x5B);
assert!(result.is_none());
let result = handler.process_byte(0x32);
assert!(result.is_none());
let result = handler.process_byte(0x30);
assert!(result.is_none());
let result = handler.process_byte(0x30);
assert!(result.is_none());
let result = handler.process_byte(0x7E);
assert!(result.is_none());
assert_eq!(handler.state(), PasteState::Pasting);
}
#[ignore] #[test]
fn test_paste_handler_paste_end_sequence() {
let mut handler = PasteHandler::new();
for byte in &[0x1B, 0x5B, 0x32, 0x30, 0x30, 0x7Eu8] {
handler.process_byte(*byte);
}
assert_eq!(handler.state(), PasteState::Pasting);
handler.buffer.push(b'h');
handler.buffer.push(b'e');
handler.buffer.push(b'l');
handler.buffer.push(b'l');
handler.buffer.push(b'o');
for byte in &[0x1B, 0x5B, 0x32, 0x30, 0x31, 0x7Eu8] {
handler.process_byte(*byte);
}
assert_eq!(handler.state(), PasteState::Normal);
}
#[ignore] #[test]
fn test_paste_handler_normal_text() {
let mut handler = PasteHandler::new();
let result = handler.process_byte(b'h');
assert_eq!(result, Some(b'h'));
let result = handler.process_byte(b'e');
assert_eq!(result, Some(b'e'));
let result = handler.process_byte(b'l');
assert_eq!(result, Some(b'l'));
}
#[test]
fn test_paste_handler_extract_image_png() {
let handler = PasteHandler::new();
let mut buffer = vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, ];
let mut handler = PasteHandler::new();
assert!(handler.extract_image_data().is_none());
let mut test_handler = PasteHandler::new();
test_handler.buffer = buffer;
let result = test_handler.extract_image_data();
assert!(result.is_some());
assert!(result.unwrap().starts_with(&[0x89, 0x50, 0x4E, 0x47]));
}
#[test]
fn test_paste_handler_extract_image_jpeg() {
let mut handler = PasteHandler::new();
let buffer = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, ];
handler.buffer = buffer;
let result = handler.extract_image_data();
assert!(result.is_some());
assert!(result.unwrap().starts_with(&[0xFF, 0xD8, 0xFF]));
}
#[test]
fn test_paste_handler_extract_image_none() {
let mut handler = PasteHandler::new();
handler.buffer = b"hello world".to_vec();
let result = handler.extract_image_data();
assert!(result.is_none());
}
#[test]
fn test_paste_handler_extract_image_short_buffer() {
let handler = PasteHandler::new();
let result = handler.extract_image_data();
assert!(result.is_none());
}
#[test]
fn test_paste_handler_extract_image_with_nulls() {
let mut handler = PasteHandler::new();
let mut buffer = vec![0u8; 100];
buffer[0] = b'A';
buffer[1] = 0;
buffer[2] = b'B';
buffer[3] = 0;
buffer[4] = b'C';
buffer[5] = 0;
for i in 10..20 {
buffer[i] = 0;
}
handler.buffer = buffer;
let result = handler.extract_image_data();
assert!(result.is_some());
}
#[test]
fn test_session_state_serialization() {
let state = SessionState {
thinking_level: "high".to_string(),
is_streaming: true,
is_compacting: false,
steering_mode: "all".to_string(),
follow_up_mode: "one_at_a_time".to_string(),
session_id: "test-123".to_string(),
session_name: Some("Test Session".to_string()),
auto_compaction_enabled: true,
message_count: 42,
pending_message_count: 1,
};
let json = serde_json::to_string(&state).unwrap();
assert!(json.contains("\"thinking_level\":\"high\""));
assert!(json.contains("\"is_streaming\":true"));
assert!(json.contains("\"message_count\":42"));
}
#[test]
fn test_command_info_serialization() {
let cmd = CommandInfo {
name: "test".to_string(),
description: Some("Test command".to_string()),
source: "builtin".to_string(),
};
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"source\":\"builtin\""));
}
#[test]
fn test_image_data_deserialization() {
let json = r#"{"source":"data:image/png;base64,ABC123","type":"image/png"}"#;
let data: ImageData = serde_json::from_str(json).unwrap();
assert_eq!(data.source, "data:image/png;base64,ABC123");
assert_eq!(data.media_type, "image/png");
}
#[test]
fn test_rpc_response_success() {
let response = RpcResponse::Response {
id: Some("123".to_string()),
command: "test".to_string(),
success: true,
data: Some(serde_json::json!({"result": "ok"})),
error: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"success\":true"));
assert!(json.contains("\"command\":\"test\""));
}
#[test]
fn test_rpc_response_error() {
let response = RpcResponse::Response {
id: Some("123".to_string()),
command: "test".to_string(),
success: false,
data: None,
error: Some("Something went wrong".to_string()),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"success\":false"));
assert!(json.contains("\"error\":\"Something went wrong\""));
}
#[test]
fn test_rpc_extension_ui_request_select() {
let request = RpcExtensionUiRequest::Select {
id: "req-123".to_string(),
title: "Select an option".to_string(),
options: vec!["Option 1".to_string(), "Option 2".to_string()],
timeout: Some(5000),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"method\":\"select\""));
assert!(json.contains("\"title\":\"Select an option\""));
}
#[test]
fn test_rpc_extension_ui_request_notify() {
let request = RpcExtensionUiRequest::Notify {
id: "req-456".to_string(),
message: "Hello!".to_string(),
notify_type: Some("info".to_string()),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"method\":\"notify\""));
assert!(json.contains("\"message\":\"Hello!\""));
}
#[test]
fn test_compaction_result_serialization() {
let result = CompactionResult {
original_count: 100,
compacted_count: 30,
tokens_saved: Some(5000),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"original_count\":100"));
assert!(json.contains("\"compacted_count\":30"));
}
#[test]
fn test_session_stats_serialization() {
let stats = SessionStats {
message_count: 50,
token_count: Some(10000),
last_activity: Some(1699000000),
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("\"message_count\":50"));
assert!(json.contains("\"token_count\":10000"));
}
}