supercode-harness 0.4.8

The optional native Supercode agent and tool harness
Documentation
//! Typed schema for Claude Code transcript JSONL
//! (`~/.claude/projects/<encoded-cwd>/<id>.jsonl`).
//!
//! Each line is a record discriminated by `type`. `user` and `assistant` carry
//! the conversation; the rest are state/metadata/UI events. Unmodeled record
//! types land in [`ClaudeRecord::Unknown`]; unmodeled fields land in `extra`.

use serde::Deserialize;

use super::ExtraFields;

/// One line of a Claude Code transcript.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ClaudeRecord {
    /// A user turn (plain text, or an array of content blocks incl. tool results).
    User {
        /// The message envelope.
        message: Message,
        /// Conversation-graph links and metadata.
        #[serde(flatten)]
        meta: RecordMeta,
    },
    /// An assistant turn (text, thinking, and/or tool calls).
    Assistant {
        /// The message envelope.
        message: Message,
        /// Conversation-graph links and metadata.
        #[serde(flatten)]
        meta: RecordMeta,
    },
    /// A system event (subtypes: `compact_boundary`, `turn_duration`,
    /// `api_error`, `away_summary`, `stop_hook_summary`, …).
    System {
        /// The event subtype.
        #[serde(default)]
        subtype: Option<String>,
        /// Anything else.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// An attachment record (pasted/added files, images, command output).
    Attachment {
        /// Anything (shape varies widely).
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A file-state snapshot enabling edit undo.
    FileHistorySnapshot {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// The per-file half of the same edit-undo state: one record per file
    /// touched by a turn, pointing back at the [`ClaudeRecord::FileHistorySnapshot`]
    /// it belongs to (`snapshotMessageId`) and naming the tracked path and its
    /// backup (`trackingPath`, `backup`). Claude Code began emitting these on
    /// 2026-07-25; before that a snapshot carried the whole set. Like its
    /// sibling it is a marker for `/rewind`, not conversation, so it is
    /// modeled generically — the point is that it stops landing in the
    /// anonymous [`ClaudeRecord::Unknown`] bucket.
    FileHistoryDelta {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Generated conversation title.
    AiTitle {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Permission-mode marker.
    PermissionMode {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Mode marker.
    Mode {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// The last typed prompt (UI restore).
    LastPrompt {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Prompt-queue operation (UI).
    QueueOperation {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// PR link metadata.
    PrLink {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Claude-hosted HTML frame link metadata (`path` + `frameUrl`). This is
    /// session/UI state rather than a conversational turn, analogous to
    /// [`ClaudeRecord::PrLink`], but remains explicitly typed so audits name
    /// its intentional cross-format residue instead of reporting an unknown
    /// record discriminant.
    FrameLink {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Subagent name marker.
    AgentName {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Subagent task start marker.
    Started {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Subagent task result marker.
    Result {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Git worktree state marker.
    WorktreeState {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// PARITY-10 (provenance P010): a lineage/provenance marker linking this
    /// session to the conversation context it was forked from (e.g. Claude
    /// Code's `--fork-session` / rewind-and-branch flow). Extremely rare in
    /// real corpora (observed 5 times in a ~12,300-file, ~1.3M-record
    /// reference corpus) and its exact field shape is unconfirmed by any
    /// sample this crate has seen — modeled generically (like
    /// [`ClaudeRecord::WorktreeState`]/[`ClaudeRecord::AgentName`]) so every
    /// field it carries, whatever they turn out to be, is captured in
    /// `extra` rather than silently landing in the anonymous
    /// [`ClaudeRecord::Unknown`] bucket.
    ForkContextRef {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Any record type we do not model yet.
    #[serde(other)]
    Unknown,
}

impl ClaudeRecord {
    /// The static discriminant name, or `None` for [`ClaudeRecord::Unknown`].
    pub fn tag(&self) -> Option<&'static str> {
        Some(match self {
            ClaudeRecord::User { .. } => "user",
            ClaudeRecord::Assistant { .. } => "assistant",
            ClaudeRecord::System { .. } => "system",
            ClaudeRecord::Attachment { .. } => "attachment",
            ClaudeRecord::FileHistorySnapshot { .. } => "file-history-snapshot",
            ClaudeRecord::FileHistoryDelta { .. } => "file-history-delta",
            ClaudeRecord::AiTitle { .. } => "ai-title",
            ClaudeRecord::PermissionMode { .. } => "permission-mode",
            ClaudeRecord::Mode { .. } => "mode",
            ClaudeRecord::LastPrompt { .. } => "last-prompt",
            ClaudeRecord::QueueOperation { .. } => "queue-operation",
            ClaudeRecord::PrLink { .. } => "pr-link",
            ClaudeRecord::FrameLink { .. } => "frame-link",
            ClaudeRecord::AgentName { .. } => "agent-name",
            ClaudeRecord::Started { .. } => "started",
            ClaudeRecord::Result { .. } => "result",
            ClaudeRecord::WorktreeState { .. } => "worktree-state",
            ClaudeRecord::ForkContextRef { .. } => "fork-context-ref",
            ClaudeRecord::Unknown => return None,
        })
    }
}

/// Conversation-graph links and per-record metadata shared by user/assistant.
#[derive(Debug, Clone, Deserialize)]
pub struct RecordMeta {
    /// This record's id.
    #[serde(default)]
    pub uuid: Option<String>,
    /// Parent record id (forms the conversation tree).
    #[serde(default)]
    pub parent_uuid: Option<String>,
    /// Whether this record belongs to a subagent (Task) sidechain rather than
    /// the main thread. **The loader does not yet separate sidechains.**
    #[serde(default)]
    pub is_sidechain: bool,
    /// Session id.
    #[serde(default)]
    pub session_id: Option<String>,
    /// Working directory.
    #[serde(default)]
    pub cwd: Option<String>,
    /// Git branch at the time.
    #[serde(default)]
    pub git_branch: Option<String>,
    /// Anything else.
    #[serde(flatten)]
    pub extra: ExtraFields,
}

/// The inner `message` object of a user/assistant record (Anthropic shape).
#[derive(Debug, Clone, Deserialize)]
pub struct Message {
    /// `user` or `assistant`.
    #[serde(default)]
    pub role: Option<String>,
    /// The model (assistant turns).
    #[serde(default)]
    pub model: Option<String>,
    /// Content: a plain string or an array of blocks.
    #[serde(default)]
    pub content: MessageContent,
    /// Anything else (usage, stop_reason, id, …).
    #[serde(flatten)]
    pub extra: ExtraFields,
}

/// A message body: plain text or a list of content blocks.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
    /// Plain string content.
    Text(String),
    /// Structured content blocks.
    Blocks(Vec<super::ContentBlock>),
}

impl Default for MessageContent {
    fn default() -> Self {
        MessageContent::Blocks(Vec::new())
    }
}