sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
+337
View File
@@ -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 {}
+34
View File
@@ -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,
};
+299
View File
@@ -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
}
}
+572
View File
@@ -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,
}