Files
openclaw/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift
2026-07-12 18:33:47 +02:00

121 lines
4.6 KiB
Swift

import ActivityKit
import Foundation
/// Shared schema used by iOS app + Live Activity widget extension.
struct OpenClawActivityAttributes: ActivityAttributes {
var agentName: String
var sessionKey: String
struct ContentState: Codable, Hashable {
enum Status: String, CaseIterable, Codable, Hashable {
case connecting
case reconnecting
case approvalNeeded
case actionRequired
case attention
case idle
case disconnected
}
var status: Status
var verbatimDetail: String?
var startedAt: Date
private enum CodingKeys: String, CodingKey {
case status
case verbatimDetail
case startedAt
}
private enum LegacyCodingKeys: String, CodingKey {
case statusText
case isIdle
case isDisconnected
case isConnecting
}
init(status: Status, verbatimDetail: String?, startedAt: Date) {
self.status = status
self.verbatimDetail = verbatimDetail
self.startedAt = startedAt
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.startedAt = try container.decode(Date.self, forKey: .startedAt)
if let status = try container.decodeIfPresent(Status.self, forKey: .status) {
self.status = status
self.verbatimDetail = try container.decodeIfPresent(String.self, forKey: .verbatimDetail)
return
}
// Live Activities can outlive an app update. Decode the shipped boolean
// schema once, then all new writes use the semantic status shape.
let legacy = try decoder.container(keyedBy: LegacyCodingKeys.self)
let statusText = try legacy.decodeIfPresent(String.self, forKey: .statusText)
let presentation = try Self.legacyPresentation(
statusText: statusText,
isIdle: legacy.decodeIfPresent(Bool.self, forKey: .isIdle) ?? false,
isDisconnected: legacy.decodeIfPresent(Bool.self, forKey: .isDisconnected) ?? false,
isConnecting: legacy.decodeIfPresent(Bool.self, forKey: .isConnecting) ?? false)
self.status = presentation.status
self.verbatimDetail = presentation.verbatimDetail
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.status, forKey: .status)
try container.encodeIfPresent(self.verbatimDetail, forKey: .verbatimDetail)
try container.encode(self.startedAt, forKey: .startedAt)
}
private static func legacyPresentation(
statusText: String?,
isIdle: Bool,
isDisconnected: Bool,
isConnecting: Bool) -> (status: Status, verbatimDetail: String?)
{
if isDisconnected {
return (.disconnected, nil)
}
if isIdle {
return (.idle, nil)
}
let trimmed = statusText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let detail = trimmed.isEmpty ? nil : trimmed
if isConnecting {
if let detail, Self.matchesShippedTranslation(detail, key: "Reconnecting...") {
return (.reconnecting, nil)
}
if let detail, Self.matchesShippedTranslation(detail, key: "Connecting...") {
return (.connecting, nil)
}
return (.connecting, detail)
}
if let detail, Self.matchesShippedTranslation(detail, key: "Approval needed") {
return (.approvalNeeded, nil)
}
if let detail, Self.matchesShippedTranslation(detail, key: "Action required") {
return (.actionRequired, nil)
}
return (.attention, detail)
}
private static func matchesShippedTranslation(_ value: String, key: String) -> Bool {
if value == key {
return true
}
return Bundle.main.localizations.contains { localization in
guard let path = Bundle.main.path(forResource: localization, ofType: "lproj"),
let bundle = Bundle(path: path)
else {
return false
}
return bundle.localizedString(forKey: key, value: key, table: nil) == value
}
}
}
}