import Foundation /// One activity-timeline entry from `GET /live-sessions/:id/events` (frozen /// contract, plan §3.1; mirrors src/types.ts:428-433). The server derives /// `class` semantically (src/types.ts:423: tool/waiting/done/stuck/user) — the /// client never re-derives it from hook names. `class` stays a raw `String` so /// the SHAPE decodes even for future/unknown classes; consumers drop unknowns /// (use `decodeList`, which does exactly that). public struct TimelineEvent: Sendable, Equatable, Decodable { /// Server ingest timestamp (ms since epoch). public let at: Int /// Server-derived semantic class; see `knownClasses`. public let `class`: String /// Sanitized tool name (server-side: ≤200 chars, control chars stripped). public let toolName: String? /// Server-derived human phrase ("ran Bash", "edited 3 files"). public let label: String /// The server's `TimelineClass` union (src/types.ts:423). public static let knownClasses: Set = ["tool", "waiting", "done", "stuck", "user"] public init(at: Int, class className: String, toolName: String?, label: String) { self.at = at self.`class` = className self.toolName = toolName self.label = label } /// True when `class` is one the server currently emits. public var hasKnownClass: Bool { Self.knownClasses.contains(`class`) } /// Decode a `/events` response body. NEVER throws: the server is an /// untrusted input source — malformed entries and unknown-class entries are /// silently dropped; a non-array body yields `[]`. public static func decodeList(from data: Data) -> [TimelineEvent] { guard let entries = try? JSONDecoder().decode([LossyEntry].self, from: data) else { return [] } return entries.compactMap(\.value).filter(\.hasKnownClass) } } /// Per-element tolerance shim: a malformed element becomes `nil` instead of /// failing the whole array decode. private struct LossyEntry: Decodable { let value: TimelineEvent? init(from decoder: any Decoder) { value = try? TimelineEvent(from: decoder) } }