sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
//! OpenCode API Client
|
||||
//!
|
||||
//! HTTP client for interacting with opencode.ai API
|
||||
|
||||
use crate::sse::{SseClient, SseError, SseStream};
|
||||
use crate::types::{MessageWithParts, Session};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Logging callback type for API client events
|
||||
pub type LogCallback = Arc<dyn Fn(&str, &str) + Send + Sync>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenCodeClient {
|
||||
base_url: String,
|
||||
client: reqwest::Client,
|
||||
log_info: Option<LogCallback>,
|
||||
log_success: Option<LogCallback>,
|
||||
log_error: Option<LogCallback>,
|
||||
}
|
||||
|
||||
impl OpenCodeClient {
|
||||
/// Create a new OpenCode API client
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
client: reqwest::Client::new(),
|
||||
log_info: None,
|
||||
log_success: None,
|
||||
log_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set logging callbacks for the client
|
||||
pub fn with_logging(
|
||||
mut self,
|
||||
log_info: LogCallback,
|
||||
log_success: LogCallback,
|
||||
log_error: LogCallback,
|
||||
) -> Self {
|
||||
self.log_info = Some(log_info);
|
||||
self.log_success = Some(log_success);
|
||||
self.log_error = Some(log_error);
|
||||
self
|
||||
}
|
||||
|
||||
fn log_info(&self, category: &str, message: &str) {
|
||||
if let Some(logger) = &self.log_info {
|
||||
logger(category, message);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_success(&self, category: &str, message: &str) {
|
||||
if let Some(logger) = &self.log_success {
|
||||
logger(category, message);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_error(&self, category: &str, message: &str) {
|
||||
if let Some(logger) = &self.log_error {
|
||||
logger(category, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// List all sessions
|
||||
pub async fn list_sessions(&self) -> Result<Vec<Session>, ClientError> {
|
||||
let url = format!("{}/session", self.base_url);
|
||||
self.log_info("API", &format!("GET {}", url));
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| String::from("(no body)"));
|
||||
self.log_error("API", &format!("GET {} failed: {}", url, status));
|
||||
self.log_error("API", &format!("Response body: {}", error_body));
|
||||
return Err(ClientError::Http(status));
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
self.log_info("API", &format!("Response: {} bytes", text.len()));
|
||||
|
||||
match serde_json::from_str::<Vec<Session>>(&text) {
|
||||
Ok(sessions) => {
|
||||
self.log_success("API", &format!("Loaded {} sessions", sessions.len()));
|
||||
Ok(sessions)
|
||||
}
|
||||
Err(e) => {
|
||||
self.log_error("API", &format!("Failed to decode sessions: {}", e));
|
||||
self.log_error("API", &format!("Response body: {}", text));
|
||||
Err(ClientError::Serialization(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a specific session by ID
|
||||
pub async fn get_session(&self, session_id: &str) -> Result<Session, ClientError> {
|
||||
let url = format!("{}/session/{}", self.base_url, session_id);
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(ClientError::Http(response.status()));
|
||||
}
|
||||
|
||||
let session = response.json::<Session>().await?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// List messages in a session
|
||||
pub async fn list_messages(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> Result<Vec<MessageWithParts>, ClientError> {
|
||||
let url = format!("{}/session/{}/message", self.base_url, session_id);
|
||||
self.log_info("API", &format!("GET {}", url));
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| String::from("(no body)"));
|
||||
self.log_error("API", &format!("GET {} failed: {}", url, status));
|
||||
self.log_error("API", &format!("Response body: {}", error_body));
|
||||
return Err(ClientError::Http(status));
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
self.log_info("API", &format!("Response: {} bytes", text.len()));
|
||||
|
||||
// Try to parse as array first to get individual message values
|
||||
match serde_json::from_str::<serde_json::Value>(&text) {
|
||||
Ok(serde_json::Value::Array(message_values)) => {
|
||||
let mut messages = Vec::new();
|
||||
for (index, msg_value) in message_values.iter().enumerate() {
|
||||
match serde_json::from_value::<MessageWithParts>(msg_value.clone()) {
|
||||
Ok(msg) => messages.push(msg),
|
||||
Err(e) => {
|
||||
self.log_error(
|
||||
"API",
|
||||
&format!("Failed to decode message at index {}: {}", index, e),
|
||||
);
|
||||
self.log_error(
|
||||
"API",
|
||||
&format!(
|
||||
"Problematic message JSON: {}",
|
||||
serde_json::to_string_pretty(msg_value)
|
||||
.unwrap_or_else(|_| msg_value.to_string())
|
||||
),
|
||||
);
|
||||
return Err(ClientError::Serialization(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.log_success("API", &format!("Loaded {} messages", messages.len()));
|
||||
Ok(messages)
|
||||
}
|
||||
Ok(_) => {
|
||||
self.log_error("API", "Response is not an array");
|
||||
self.log_error("API", &format!("Response body: {}", text));
|
||||
// Create a dummy error by trying to parse invalid data
|
||||
let dummy_err = serde_json::from_str::<Vec<MessageWithParts>>("null").unwrap_err();
|
||||
Err(ClientError::Serialization(dummy_err))
|
||||
}
|
||||
Err(e) => {
|
||||
self.log_error("API", &format!("Failed to parse JSON: {}", e));
|
||||
self.log_error(
|
||||
"API",
|
||||
&format!(
|
||||
"Response body (first 1000 chars): {}",
|
||||
if text.len() > 1000 {
|
||||
&text[..1000]
|
||||
} else {
|
||||
&text
|
||||
}
|
||||
),
|
||||
);
|
||||
Err(ClientError::Serialization(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a chat message to a session
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
session_id: &str,
|
||||
text: String,
|
||||
) -> Result<MessageWithParts, ClientError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ChatPart {
|
||||
#[serde(rename = "type")]
|
||||
part_type: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ChatInput {
|
||||
parts: Vec<ChatPart>,
|
||||
}
|
||||
|
||||
let url = format!("{}/session/{}/message", self.base_url, session_id);
|
||||
self.log_info("API", &format!("POST {} (text: {} chars)", url, text.len()));
|
||||
|
||||
let input = ChatInput {
|
||||
parts: vec![ChatPart {
|
||||
part_type: "text".to_string(),
|
||||
text,
|
||||
}],
|
||||
};
|
||||
|
||||
let response = self.client.post(&url).json(&input).send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| String::from("(no body)"));
|
||||
self.log_error("API", &format!("POST {} failed: {}", url, status));
|
||||
self.log_error("API", &format!("Response body: {}", error_body));
|
||||
return Err(ClientError::Http(status));
|
||||
}
|
||||
|
||||
let response_text = response.text().await?;
|
||||
self.log_info("API", &format!("Response: {} bytes", response_text.len()));
|
||||
|
||||
match serde_json::from_str::<MessageWithParts>(&response_text) {
|
||||
Ok(message) => {
|
||||
self.log_success("API", "Message sent successfully");
|
||||
Ok(message)
|
||||
}
|
||||
Err(e) => {
|
||||
self.log_error("API", &format!("Failed to decode message: {}", e));
|
||||
self.log_error("API", &format!("Response body: {}", response_text));
|
||||
Err(ClientError::Serialization(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to server-sent events (SSE) for real-time updates
|
||||
///
|
||||
/// Returns a stream of events including message updates, session changes, etc.
|
||||
/// The stream will automatically handle the SSE protocol and deserialize events.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use opencode_client::OpenCodeClient;
|
||||
/// # use futures::stream::StreamExt;
|
||||
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = OpenCodeClient::new("http://localhost:12225");
|
||||
/// let mut stream = client.subscribe_events()?;
|
||||
///
|
||||
/// while let Some(event_result) = stream.next().await {
|
||||
/// match event_result {
|
||||
/// Ok(event) => println!("Received event: {:?}", event),
|
||||
/// Err(e) => eprintln!("Event error: {}", e),
|
||||
/// }
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn subscribe_events(&self) -> Result<SseStream, SseError> {
|
||||
self.log_info("SSE", &format!("Connecting to {}/event", self.base_url));
|
||||
|
||||
let sse_client = SseClient::new(&self.base_url);
|
||||
let stream = sse_client.connect()?;
|
||||
|
||||
self.log_success("SSE", "Connected to event stream");
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Abort an in-progress message generation in a session
|
||||
///
|
||||
/// Sends a POST request to `/session/:id/abort` to cancel the current generation.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(true)` if the abort was successful, `Ok(false)` if there was nothing to abort,
|
||||
/// or an error if the request failed.
|
||||
pub async fn abort_session(&self, session_id: &str) -> Result<bool, ClientError> {
|
||||
let url = format!("{}/session/{}/abort", self.base_url, session_id);
|
||||
self.log_info("API", &format!("POST {} (abort)", url));
|
||||
|
||||
let response = self.client.post(&url).send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| String::from("(no body)"));
|
||||
self.log_error("API", &format!("POST {} failed: {}", url, status));
|
||||
self.log_error("API", &format!("Response body: {}", error_body));
|
||||
return Err(ClientError::Http(status));
|
||||
}
|
||||
|
||||
self.log_success("API", "Generation aborted successfully");
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
Http(reqwest::StatusCode),
|
||||
Request(reqwest::Error),
|
||||
Serialization(serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ClientError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
ClientError::Request(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ClientError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ClientError::Serialization(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ClientError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ClientError::Http(status) => write!(f, "HTTP error: {}", status),
|
||||
ClientError::Request(err) => write!(f, "Request error: {}", err),
|
||||
ClientError::Serialization(err) => write!(f, "Serialization error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ClientError {}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! OpenCode API Client Library
|
||||
//!
|
||||
//! A Rust client for interacting with the OpenCode.ai API.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use opencode_client::OpenCodeClient;
|
||||
//!
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let client = OpenCodeClient::new("http://localhost:12225");
|
||||
//!
|
||||
//! // List all sessions
|
||||
//! let sessions = client.list_sessions().await?;
|
||||
//!
|
||||
//! // Get messages from a session
|
||||
//! let messages = client.list_messages("session_id").await?;
|
||||
//!
|
||||
//! // Send a message
|
||||
//! let response = client.send_message("session_id", "Hello!".to_string()).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod client;
|
||||
pub mod sse;
|
||||
pub mod types;
|
||||
|
||||
pub use client::{ClientError, LogCallback, OpenCodeClient};
|
||||
pub use sse::{ConnectionState, SseClient, SseError, SseStream};
|
||||
pub use types::{
|
||||
AssistantMessage, AssistantMessageTime, Event, Message, MessageTime, MessageWithParts, Part,
|
||||
ReasoningPart, Session, TextPart, ToolPart, ToolState, UserMessage,
|
||||
};
|
||||
@@ -0,0 +1,299 @@
|
||||
//! SSE (Server-Sent Events) client for OpenCode API
|
||||
//!
|
||||
//! Provides cross-platform SSE streaming for real-time event updates
|
||||
|
||||
use crate::types::Event;
|
||||
use futures::stream::Stream;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// SSE-specific errors
|
||||
#[derive(Debug)]
|
||||
pub enum SseError {
|
||||
Network(String),
|
||||
Parse(String),
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SseError::Network(msg) => write!(f, "Network error: {}", msg),
|
||||
SseError::Parse(msg) => write!(f, "Parse error: {}", msg),
|
||||
SseError::Closed => write!(f, "Connection closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SseError {}
|
||||
|
||||
/// Connection state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Stream of SSE events
|
||||
pub type SseStream = Pin<Box<dyn Stream<Item = Result<Event, SseError>> + Send>>;
|
||||
|
||||
/// SSE Client
|
||||
pub struct SseClient {
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl SseClient {
|
||||
/// Create new SSE client
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to SSE endpoint and return event stream
|
||||
pub fn connect(&self) -> Result<SseStream, SseError> {
|
||||
let url = format!("{}/event", self.base_url);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
wasm::connect_sse(&url)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
native::connect_sse(&url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm {
|
||||
use super::*;
|
||||
use futures::channel::mpsc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{EventSource, MessageEvent};
|
||||
|
||||
pub fn connect_sse(url: &str) -> Result<SseStream, SseError> {
|
||||
web_sys::console::log_1(&format!("SSE: Creating EventSource for {}", url).into());
|
||||
|
||||
let event_source = EventSource::new(url)
|
||||
.map_err(|e| SseError::Network(format!("EventSource creation failed: {:?}", e)))?;
|
||||
|
||||
web_sys::console::log_1(
|
||||
&format!(
|
||||
"SSE: EventSource created, readyState: {}",
|
||||
event_source.ready_state()
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let (tx, rx) = mpsc::unbounded::<Result<Event, SseError>>();
|
||||
|
||||
setup_listeners(&event_source, tx)?;
|
||||
|
||||
Ok(Box::pin(rx))
|
||||
}
|
||||
|
||||
fn setup_listeners(
|
||||
source: &EventSource,
|
||||
mut tx: mpsc::UnboundedSender<Result<Event, SseError>>,
|
||||
) -> Result<(), SseError> {
|
||||
// Add open event listener
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(move |_e: web_sys::Event| {
|
||||
web_sys::console::log_1(&"SSE: Connection opened".into());
|
||||
}) as Box<dyn FnMut(web_sys::Event)>);
|
||||
|
||||
source
|
||||
.add_event_listener_with_callback("open", cb.as_ref().unchecked_ref())
|
||||
.map_err(|e| SseError::Network(format!("Open listener setup failed: {:?}", e)))?;
|
||||
|
||||
cb.forget();
|
||||
}
|
||||
|
||||
// Add error event listener
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(move |e: web_sys::Event| {
|
||||
web_sys::console::error_1(&format!("SSE: Error event: {:?}", e).into());
|
||||
}) as Box<dyn FnMut(web_sys::Event)>);
|
||||
|
||||
source
|
||||
.add_event_listener_with_callback("error", cb.as_ref().unchecked_ref())
|
||||
.map_err(|e| SseError::Network(format!("Error listener setup failed: {:?}", e)))?;
|
||||
|
||||
cb.forget();
|
||||
}
|
||||
|
||||
let events = [
|
||||
"message.updated",
|
||||
"message.removed",
|
||||
"message.part.updated",
|
||||
"message.part.removed",
|
||||
"session.updated",
|
||||
"session.created",
|
||||
"session.deleted",
|
||||
"session.compacted",
|
||||
"server.connected",
|
||||
"session.idle",
|
||||
"session.error",
|
||||
"permission.updated",
|
||||
"file.edited",
|
||||
"todo.updated",
|
||||
];
|
||||
|
||||
web_sys::console::log_1(
|
||||
&format!("SSE: Setting up listeners for {} event types", events.len()).into(),
|
||||
);
|
||||
|
||||
for event_type in events {
|
||||
let tx_clone = tx.clone();
|
||||
let evt = event_type.to_string();
|
||||
let evt_for_log = evt.clone();
|
||||
|
||||
let cb = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
web_sys::console::log_1(&format!("SSE: Received {} event", evt_for_log).into());
|
||||
|
||||
if let Some(data) = e.data().as_string() {
|
||||
web_sys::console::log_1(&format!("SSE: Event data: {}", data).into());
|
||||
|
||||
// OpenCode sends the event type INSIDE the data JSON, not in the event: field
|
||||
// So we parse the data directly instead of wrapping it
|
||||
match serde_json::from_str(&data) {
|
||||
Ok(event) => {
|
||||
web_sys::console::log_1(
|
||||
&format!("SSE: Parsed event successfully").into(),
|
||||
);
|
||||
let _ = tx_clone.unbounded_send(Ok(event));
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("SSE: Parse error: {}", e).into());
|
||||
let _ = tx_clone.unbounded_send(Err(SseError::Parse(e.to_string())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
web_sys::console::warn_1(
|
||||
&format!("SSE: {} event has no data", evt_for_log).into(),
|
||||
);
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
source
|
||||
.add_event_listener_with_callback(event_type, cb.as_ref().unchecked_ref())
|
||||
.map_err(|e| SseError::Network(format!("Listener setup failed: {:?}", e)))?;
|
||||
|
||||
cb.forget();
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&"SSE: All listeners setup complete".into());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native {
|
||||
use super::*;
|
||||
use futures::channel::mpsc;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::SinkExt;
|
||||
|
||||
pub fn connect_sse(url: &str) -> Result<SseStream, SseError> {
|
||||
let url = url.to_string();
|
||||
let (tx, rx) = mpsc::unbounded::<Result<Event, SseError>>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = stream_events(url, tx).await;
|
||||
});
|
||||
|
||||
Ok(Box::pin(rx))
|
||||
}
|
||||
|
||||
async fn stream_events(
|
||||
url: String,
|
||||
mut tx: mpsc::UnboundedSender<Result<Event, SseError>>,
|
||||
) -> Result<(), SseError> {
|
||||
let response = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| SseError::Network(e.to_string()))?;
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut buffer = String::new();
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| SseError::Network(e.to_string()))?;
|
||||
buffer.push_str(&String::from_utf8_lossy(&chunk));
|
||||
|
||||
while let Some((event, remaining)) = parse_sse(&buffer) {
|
||||
buffer = remaining;
|
||||
if tx.send(Ok(event)).await.is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send(Err(SseError::Closed)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Filter system prompts from JSON data for cleaner logging
|
||||
fn filter_system_prompt(data: &str) -> String {
|
||||
// Try to parse as JSON and filter out system prompts
|
||||
match serde_json::from_str::<serde_json::Value>(data) {
|
||||
Ok(mut json) => {
|
||||
if let Some(properties) = json.get_mut("properties") {
|
||||
if let Some(info) = properties.get_mut("info") {
|
||||
if let Some(system) = info.get_mut("system") {
|
||||
if let Some(arr) = system.as_array() {
|
||||
if !arr.is_empty() {
|
||||
*system = serde_json::json!(format!("[FILTERED {} items]", arr.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&json).unwrap_or_else(|_| data.to_string())
|
||||
}
|
||||
Err(_) => data.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sse(buffer: &str) -> Option<(Event, String)> {
|
||||
let mut event_type = String::new();
|
||||
let mut data_lines = Vec::new();
|
||||
let mut pos = 0;
|
||||
|
||||
for line in buffer.lines() {
|
||||
pos += line.len() + 1;
|
||||
|
||||
if line.is_empty() {
|
||||
if !data_lines.is_empty() {
|
||||
let data = data_lines.join("\n");
|
||||
|
||||
// Filter system prompts for cleaner logging
|
||||
let filtered_data = filter_system_prompt(&data);
|
||||
eprintln!("[OpenCode SSE] event_type='{}', data={}", event_type, filtered_data);
|
||||
|
||||
// OpenCode sends the event type INSIDE the data JSON, not in the event: field
|
||||
// So we parse the data directly instead of wrapping it
|
||||
match serde_json::from_str(&data) {
|
||||
Ok(event) => {
|
||||
eprintln!("[OpenCode SSE] Parsed event successfully");
|
||||
return Some((event, buffer[pos..].to_string()));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[OpenCode SSE] Parse error: {} for data: {}", e, filtered_data);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(val) = line.strip_prefix("event:") {
|
||||
event_type = val.trim().to_string();
|
||||
} else if let Some(val) = line.strip_prefix("data:") {
|
||||
data_lines.push(val.trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
//! OpenCode API Types
|
||||
//!
|
||||
//! Rust representations of the OpenCode API types based on opencode_types.ts
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
#[serde(rename = "projectID")]
|
||||
pub project_id: String,
|
||||
pub directory: String,
|
||||
#[serde(rename = "parentID", skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<SessionSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub share: Option<SessionShare>,
|
||||
pub title: String,
|
||||
pub version: String,
|
||||
pub time: SessionTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub revert: Option<SessionRevert>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionSummary {
|
||||
pub diffs: Vec<FileDiff>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionShare {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionTime {
|
||||
pub created: u64,
|
||||
pub updated: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub compacting: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionRevert {
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
#[serde(rename = "partID", skip_serializing_if = "Option::is_none")]
|
||||
pub part_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub snapshot: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub diff: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FileDiff {
|
||||
#[serde(alias = "path")]
|
||||
pub file: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub additions: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deletions: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub before: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub after: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "role")]
|
||||
pub enum Message {
|
||||
#[serde(rename = "user")]
|
||||
User(UserMessage),
|
||||
#[serde(rename = "assistant")]
|
||||
Assistant(AssistantMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct UserMessage {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
pub time: MessageTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<UserMessageSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct UserMessageSummary {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<String>,
|
||||
pub diffs: Vec<FileDiff>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AssistantMessage {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
pub time: AssistantMessageTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<MessageError>,
|
||||
#[serde(default)]
|
||||
pub system: Vec<String>,
|
||||
#[serde(rename = "parentID", skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<String>,
|
||||
#[serde(rename = "modelID", skip_serializing_if = "Option::is_none")]
|
||||
pub model_id: Option<String>,
|
||||
#[serde(rename = "providerID", skip_serializing_if = "Option::is_none")]
|
||||
pub provider_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<MessagePath>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub cost: f64,
|
||||
#[serde(default)]
|
||||
pub tokens: TokenUsage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageTime {
|
||||
pub created: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AssistantMessageTime {
|
||||
pub created: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completed: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessagePath {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cwd: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct TokenUsage {
|
||||
pub input: u64,
|
||||
pub output: u64,
|
||||
pub reasoning: u64,
|
||||
#[serde(default)]
|
||||
pub cache: CacheUsage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct CacheUsage {
|
||||
pub read: u64,
|
||||
pub write: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "name")]
|
||||
pub enum MessageError {
|
||||
ProviderAuthError { data: ProviderAuthErrorData },
|
||||
UnknownError { data: UnknownErrorData },
|
||||
MessageOutputLengthError,
|
||||
MessageAbortedError { data: MessageAbortedErrorData },
|
||||
ApiError { data: ApiErrorData },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ProviderAuthErrorData {
|
||||
#[serde(rename = "providerID")]
|
||||
pub provider_id: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct UnknownErrorData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageAbortedErrorData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ApiErrorData {
|
||||
pub message: String,
|
||||
#[serde(rename = "statusCode", skip_serializing_if = "Option::is_none")]
|
||||
pub status_code: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Part {
|
||||
#[serde(rename = "text")]
|
||||
Text(TextPart),
|
||||
#[serde(rename = "reasoning")]
|
||||
Reasoning(ReasoningPart),
|
||||
#[serde(rename = "step-start")]
|
||||
StepStart(GenericPart),
|
||||
#[serde(rename = "step-finish")]
|
||||
StepFinish(GenericPart),
|
||||
#[serde(rename = "tool")]
|
||||
Tool(ToolPart),
|
||||
#[serde(rename = "file")]
|
||||
File(FilePart),
|
||||
#[serde(rename = "snapshot")]
|
||||
Snapshot(SnapshotPart),
|
||||
#[serde(rename = "patch")]
|
||||
Patch(PatchPart),
|
||||
#[serde(rename = "agent")]
|
||||
Agent(AgentPart),
|
||||
#[serde(rename = "retry")]
|
||||
Retry(RetryPart),
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PartTime {
|
||||
pub start: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GenericPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TextPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub synthetic: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub time: Option<PartTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ReasoningPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub time: Option<PartTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum ToolState {
|
||||
#[serde(rename = "pending")]
|
||||
Pending,
|
||||
#[serde(rename = "running")]
|
||||
Running {
|
||||
input: serde_json::Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
metadata: Option<serde_json::Value>,
|
||||
time: PartTime,
|
||||
},
|
||||
#[serde(rename = "completed")]
|
||||
Completed {
|
||||
input: serde_json::Value,
|
||||
output: String,
|
||||
title: String,
|
||||
metadata: serde_json::Value,
|
||||
time: PartTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
attachments: Option<Vec<serde_json::Value>>,
|
||||
},
|
||||
#[serde(rename = "error")]
|
||||
Error {
|
||||
input: serde_json::Value,
|
||||
error: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
metadata: Option<serde_json::Value>,
|
||||
time: PartTime,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ToolPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
#[serde(rename = "callID")]
|
||||
pub call_id: String,
|
||||
pub tool: String,
|
||||
pub state: ToolState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FilePart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub mime: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filename: Option<String>,
|
||||
pub url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<serde_json::Value>, // Simplified - can expand later
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SnapshotPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub snapshot: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PatchPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub hash: String,
|
||||
pub files: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AgentPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<AgentSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AgentSource {
|
||||
pub value: String,
|
||||
pub start: u64,
|
||||
pub end: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct RetryPart {
|
||||
pub id: String,
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
pub attempt: u32,
|
||||
pub error: ApiErrorData, // Reusing existing type
|
||||
pub time: RetryTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct RetryTime {
|
||||
pub created: u64,
|
||||
}
|
||||
|
||||
/// Response containing message info and its parts
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageWithParts {
|
||||
pub info: Message,
|
||||
pub parts: Vec<Part>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SSE Event Types
|
||||
// ============================================================================
|
||||
|
||||
/// SSE Event types from OpenCode API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Event {
|
||||
#[serde(rename = "server.connected")]
|
||||
ServerConnected { properties: serde_json::Value },
|
||||
#[serde(rename = "session.created")]
|
||||
SessionCreated { properties: SessionEventInfo },
|
||||
#[serde(rename = "session.updated")]
|
||||
SessionUpdated { properties: SessionEventInfo },
|
||||
#[serde(rename = "session.deleted")]
|
||||
SessionDeleted { properties: SessionEventInfo },
|
||||
#[serde(rename = "session.compacted")]
|
||||
SessionCompacted { properties: SessionIdOnly },
|
||||
#[serde(rename = "session.idle")]
|
||||
SessionIdle { properties: SessionIdOnly },
|
||||
#[serde(rename = "session.error")]
|
||||
SessionError { properties: SessionErrorInfo },
|
||||
#[serde(rename = "message.updated")]
|
||||
MessageUpdated { properties: MessageEventInfo },
|
||||
#[serde(rename = "message.removed")]
|
||||
MessageRemoved { properties: MessageRemovedInfo },
|
||||
#[serde(rename = "message.part.updated")]
|
||||
MessagePartUpdated { properties: MessagePartEventInfo },
|
||||
#[serde(rename = "message.part.removed")]
|
||||
MessagePartRemoved { properties: MessagePartRemovedInfo },
|
||||
#[serde(rename = "permission.updated")]
|
||||
PermissionUpdated { properties: Permission },
|
||||
#[serde(rename = "permission.replied")]
|
||||
PermissionReplied { properties: PermissionReplyInfo },
|
||||
#[serde(rename = "file.edited")]
|
||||
FileEdited { properties: FileEditedInfo },
|
||||
#[serde(rename = "file.watcher.updated")]
|
||||
FileWatcherUpdated { properties: FileWatcherInfo },
|
||||
#[serde(rename = "todo.updated")]
|
||||
TodoUpdated { properties: TodoEventInfo },
|
||||
#[serde(rename = "lsp.client.diagnostics")]
|
||||
LspClientDiagnostics { properties: LspDiagnosticsInfo },
|
||||
#[serde(rename = "installation.updated")]
|
||||
InstallationUpdated { properties: InstallationInfo },
|
||||
#[serde(rename = "ide.installed")]
|
||||
IdeInstalled { properties: IdeInfo },
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionEventInfo {
|
||||
pub info: Session,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionIdOnly {
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionErrorInfo {
|
||||
#[serde(rename = "sessionID", skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<MessageError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageEventInfo {
|
||||
pub info: Message,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageRemovedInfo {
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessagePartEventInfo {
|
||||
pub part: Part,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delta: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessagePartRemovedInfo {
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
#[serde(rename = "partID")]
|
||||
pub part_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Permission {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub permission_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pattern: Option<serde_json::Value>, // Can be string or array
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "messageID")]
|
||||
pub message_id: String,
|
||||
#[serde(rename = "callID", skip_serializing_if = "Option::is_none")]
|
||||
pub call_id: Option<String>,
|
||||
pub title: String,
|
||||
pub metadata: serde_json::Value,
|
||||
pub time: PermissionTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PermissionTime {
|
||||
pub created: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PermissionReplyInfo {
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
#[serde(rename = "permissionID")]
|
||||
pub permission_id: String,
|
||||
pub response: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FileEditedInfo {
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FileWatcherInfo {
|
||||
pub file: String,
|
||||
pub event: FileWatcherEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FileWatcherEvent {
|
||||
Add,
|
||||
Change,
|
||||
Unlink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TodoEventInfo {
|
||||
#[serde(rename = "sessionID")]
|
||||
pub session_id: String,
|
||||
pub todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Todo {
|
||||
pub content: String,
|
||||
pub status: String,
|
||||
pub priority: String,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LspDiagnosticsInfo {
|
||||
#[serde(rename = "serverID")]
|
||||
pub server_id: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct InstallationInfo {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct IdeInfo {
|
||||
pub ide: String,
|
||||
}
|
||||
Reference in New Issue
Block a user