use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionStatus {
Recording,
Ok,
Error,
Interrupted,
}
impl fmt::Display for SessionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Recording => "recording",
Self::Ok => "ok",
Self::Error => "error",
Self::Interrupted => "interrupted",
})
}
}
impl FromStr for SessionStatus {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"recording" => Ok(Self::Recording),
"ok" => Ok(Self::Ok),
"error" => Ok(Self::Error),
"interrupted" => Ok(Self::Interrupted),
other => Err(format!("unknown session status `{other}`")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AgentKind {
ClaudeCode,
Generic,
McpOnly,
}
impl fmt::Display for AgentKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::ClaudeCode => "claude-code",
Self::Generic => "generic",
Self::McpOnly => "mcp-only",
})
}
}
impl FromStr for AgentKind {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"claude-code" => Ok(Self::ClaudeCode),
"generic" => Ok(Self::Generic),
"mcp-only" => Ok(Self::McpOnly),
other => Err(format!("unknown agent kind `{other}`")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AdapterStatus {
#[default]
None,
Active,
Degraded,
}
impl fmt::Display for AdapterStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::None => "none",
Self::Active => "active",
Self::Degraded => "degraded",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
Lifecycle,
UserMessage,
AgentMessage,
Thinking,
ToolCall,
ToolResult,
McpRequest,
McpResponse,
McpNotification,
FileChange,
TerminalOutput,
Error,
}
impl fmt::Display for EventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Lifecycle => "lifecycle",
Self::UserMessage => "user_message",
Self::AgentMessage => "agent_message",
Self::Thinking => "thinking",
Self::ToolCall => "tool_call",
Self::ToolResult => "tool_result",
Self::McpRequest => "mcp_request",
Self::McpResponse => "mcp_response",
Self::McpNotification => "mcp_notification",
Self::FileChange => "file_change",
Self::TerminalOutput => "terminal_output",
Self::Error => "error",
})
}
}
impl FromStr for EventKind {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"lifecycle" => Ok(Self::Lifecycle),
"user_message" => Ok(Self::UserMessage),
"agent_message" => Ok(Self::AgentMessage),
"thinking" => Ok(Self::Thinking),
"tool_call" => Ok(Self::ToolCall),
"tool_result" => Ok(Self::ToolResult),
"mcp_request" => Ok(Self::McpRequest),
"mcp_response" => Ok(Self::McpResponse),
"mcp_notification" => Ok(Self::McpNotification),
"file_change" => Ok(Self::FileChange),
"terminal_output" => Ok(Self::TerminalOutput),
"error" => Ok(Self::Error),
other => Err(format!("unknown event kind `{other}`")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChangeKind {
Created,
Modified,
Deleted,
}
impl fmt::Display for ChangeKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Created => "created",
Self::Modified => "modified",
Self::Deleted => "deleted",
})
}
}
impl FromStr for ChangeKind {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"created" => Ok(Self::Created),
"modified" => Ok(Self::Modified),
"deleted" => Ok(Self::Deleted),
other => Err(format!("unknown change kind `{other}`")),
}
}
}
#[derive(Debug, Clone)]
pub struct NewSession {
pub id: Uuid,
pub started_at: i64,
pub agent_kind: AgentKind,
pub adapter_status: AdapterStatus,
pub command: Vec<String>,
pub cwd: PathBuf,
pub hostname: Option<String>,
pub hh_version: String,
pub model: Option<String>,
pub git_branch: Option<String>,
pub git_sha: Option<String>,
pub git_dirty: Option<bool>,
}
#[must_use]
pub fn now_v7() -> Uuid {
Uuid::now_v7()
}
impl NewSession {
pub fn short_id(&self) -> String {
let simple = self.id.simple().to_string();
simple[26..32].to_string()
}
}
#[derive(Debug, Clone)]
pub struct SessionRow {
pub id: String,
pub short_id: String,
pub started_at: i64,
pub ended_at: Option<i64>,
pub exit_code: Option<i32>,
pub status: SessionStatus,
pub agent_kind: AgentKind,
pub adapter_status: AdapterStatus,
pub command: Vec<String>,
pub cwd: PathBuf,
pub step_count: i64,
pub files_changed: i64,
}
#[derive(Debug, Clone, Serialize)]
pub struct Event {
pub session_id: String,
pub ts_ms: i64,
pub kind: EventKind,
pub step: Option<i64>,
pub summary: String,
pub body_json: Option<serde_json::Value>,
pub blob_hash: Option<String>,
pub blob_size: Option<u64>,
pub correlates: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct EventRow {
pub id: i64,
pub session_id: String,
pub ts_ms: i64,
pub kind: EventKind,
pub step: Option<i64>,
pub correlates: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct EventIndexRow {
pub id: i64,
pub ts_ms: i64,
pub kind: EventKind,
pub step: Option<i64>,
pub correlates: Option<i64>,
pub summary: String,
}
#[derive(Debug, Clone)]
pub struct EventDetail {
pub id: i64,
pub session_id: String,
pub ts_ms: i64,
pub kind: EventKind,
pub step: Option<i64>,
pub correlates: Option<i64>,
pub summary: String,
pub body_json: Option<serde_json::Value>,
pub file_change: Option<FileChange>,
}
#[derive(Debug, Clone)]
pub struct FileChange {
pub event_id: i64,
pub path: String,
pub change_kind: ChangeKind,
pub before_hash: Option<String>,
pub after_hash: Option<String>,
pub is_binary: bool,
}
#[must_use]
pub fn truncate_summary(s: &str) -> String {
const LIMIT: usize = 120;
if s.chars().count() <= LIMIT {
return s.to_string();
}
let truncated: String = s.chars().take(LIMIT - 1).collect();
format!("{truncated}…")
}