import Foundation // What the iPhone's Live Activity (Lock Screen and Dynamic Island) shows: // the most urgent agent session, while the Mac is locked. The Mac builds it, // the relay forwards it as the Live Activity's "content-state", ActivityKit // decodes it on the iPhone. It passes through the relay and Apple's push // service, so it holds no project name, step text, command or path: only the // agent, its state and counts. struct MochiActivityState: Codable, Hashable, Sendable { var pillId: String var agent: String // "VS Code", "Codex"… var color: String // agent color, hex var state: String // BotState raw value var statusText: String // "working · 3/7", "waiting for your OK"… var tone: String // waiting, question, error, working, done, idle var stepIndex: Int var stepCount: Int var others: Int // other sessions still going /// When Mochi left for the iPhone (Unix seconds): the iPhone counts the /// time from it, live. Optional so older pushes still decode. var since: Int? = nil /// The fingerprint of the command waiting for your OK (an opaque hash, /// never the command): the Lock Screen's Allow and Deny answer this one. var approval: String? = nil var botState: BotState { BotState(rawValue: state) ?? .idle } var isActive: Bool { ["waiting", "question", "working", "error"].contains(tone) } /// Lower = more urgent, same order as the iPhone list. static func urgency(state: BotState, waitingForOK: Bool, hasQuestion: Bool) -> Int { if waitingForOK || state == .approval { return 0 } if hasQuestion || state == .question { return 1 } switch state { case .error, .ratelimit, .dizzy: return 2 case .working, .thinking, .searching: return 3 case .finished: return 4 default: return 5 } } static func tone(urgency: Int) -> String { ["waiting", "question", "error", "working", "done"].indices.contains(urgency) ? ["waiting", "question", "error", "working", "done"][urgency] : "idle" } static func statusText(state: BotState, urgency: Int, stepIndex: Int, stepCount: Int) -> String { switch urgency { case 0: return "ждёт вашего OK" case 1: return "есть вопрос" default: break } switch state { case .working, .thinking, .searching: return stepCount == 0 ? "работает" : "работает · \(min(stepIndex + 1, stepCount))/\(stepCount)" case .finished: return "✓ готово" case .error: return "ошибка" case .ratelimit: return "лимит запросов" case .sleeping: return "спит" case .dizzy: return "голова кружится" default: return "отдыхает" } } static let placeholder = MochiActivityState( pillId: "integration_claude", agent: "VS Code", color: "#4A86E8", state: "working", statusText: "работает · 3/7", tone: "working", stepIndex: 2, stepCount: 7, others: 1) }