use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct TerminalStartOptions {
pub cwd: String,
pub command: Vec<String>,
pub env: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct TerminalSession {
pub id: String,
}
pub trait TerminalBackend: Send + Sync {
fn start(&self, options: TerminalStartOptions) -> Result<TerminalSession, ClaudeTerminalError>;
fn paste_text(&self, session: &TerminalSession, text: &str) -> Result<(), ClaudeTerminalError>;
fn submit(&self, session: &TerminalSession) -> Result<(), ClaudeTerminalError>;
fn capture_screen(&self, session: &TerminalSession) -> Result<String, ClaudeTerminalError>;
fn stop(&self, session: &TerminalSession) -> Result<(), ClaudeTerminalError>;
}
#[derive(Debug, Clone)]
pub struct ClaudeSessionRef {
pub session_id: String,
pub transcript_path: String,
}
#[derive(Debug, Clone)]
pub struct FindClaudeSessionOptions {
pub cwd: String,
pub after_ms: u64,
pub timeout_ms: u64,
pub poll_interval_ms: u64,
pub root: String,
pub exclude_names: std::collections::HashSet<String>,
}
#[derive(Debug, Clone)]
pub struct WaitForAssistantResponseOptions {
pub timeout_ms: u64,
pub poll_interval_ms: u64,
}
pub trait ClaudeTranscriptReader: Send + Sync {
fn list_session_names(
&self,
root: &str,
cwd: &str,
) -> Result<std::collections::HashSet<String>, ClaudeTerminalError>;
fn find_session(
&self,
options: FindClaudeSessionOptions,
) -> Result<ClaudeSessionRef, ClaudeTerminalError>;
fn wait_for_assistant_response(
&self,
session: &ClaudeSessionRef,
options: WaitForAssistantResponseOptions,
) -> Result<ClaudeTerminalResponse, ClaudeTerminalError>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptMessage {
#[serde(rename = "type")]
pub msg_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
#[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageContent {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ClaudeTerminalResponse {
pub session_id: String,
pub assistant_messages: Vec<TranscriptMessage>,
pub last_result_message: Option<TranscriptMessage>,
}
#[derive(Debug, thiserror::Error)]
pub enum ClaudeTerminalError {
#[error("{0}")]
Other(String),
#[error("{0}")]
Timeout(String),
#[error("{}", format_session_limit(.reset_info.as_deref()))]
SessionLimit { reset_info: Option<String> },
}
fn format_session_limit(reset_info: Option<&str>) -> String {
match reset_info {
Some(r) => format!("Claude session limit reached (resets {r})"),
None => "Claude session limit reached".to_string(),
}
}
impl ClaudeTerminalError {
#[must_use]
pub fn is_session_limit(&self) -> bool {
matches!(self, Self::SessionLimit { .. })
}
#[must_use]
pub fn reset_info(&self) -> Option<&str> {
match self {
Self::SessionLimit { reset_info } => reset_info.as_deref(),
_ => None,
}
}
}