Watch: waiting-count complication and multi-select questions

A WidgetKit extension (accessoryCircular, Rectangular, Inline, Corner) shows
GET /v1/summary. The device token moves to the App Group's Keychain access
group on launch so the extension can read it without re-pairing; the app
reloads the timeline when its feed counts change and on notifications.
Multi-select questions toggle options and send an array of values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
gibandClaude Opus 5.5 committed 2026-11-01 11:06:38 -05:00
1 parent d4d67295c6
commit 0994fd7d5a
18 files changed
+800 -23

No files matched your search

+66 -1
View File
@@ -80,6 +80,8 @@ struct PendingRequest: Codable, Hashable, Sendable, Identifiable {
var question: String
var options: [Choice]
var allowCustomAnswer: Bool
/// When true, the answer is an array of option values.
var multiSelect = false
}
var id: String
@@ -113,6 +115,24 @@ struct PendingRequest: Codable, Hashable, Sendable, Identifiable {
}
}
extension PendingRequest.Question {
private enum CodingKeys: String, CodingKey {
case id, header, question, options, allowCustomAnswer, multiSelect
}
/// `multiSelect` is newer than the rest, so a gateway that leaves it out means single-select.
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.init(
id: try container.decode(String.self, forKey: .id),
header: try container.decode(String.self, forKey: .header),
question: try container.decode(String.self, forKey: .question),
options: try container.decode([Choice].self, forKey: .options),
allowCustomAnswer: try container.decode(Bool.self, forKey: .allowCustomAnswer),
multiSelect: try container.decodeIfPresent(Bool.self, forKey: .multiSelect) ?? false)
}
}
struct FeedItem: Codable, Hashable, Sendable, Identifiable {
var hostId: String
var hostLabel: String
@@ -136,6 +156,37 @@ struct Feed: Codable, Hashable, Sendable {
var hosts: [HostSummary]
}
/// `GET /v1/summary`: the counts behind the complication.
struct Summary: Codable, Hashable, Sendable {
struct Next: Codable, Hashable, Sendable {
var hostId: String
var threadId: String
var title: String
var hostLabel: String
var route: Route { .thread(hostId: hostId, threadId: threadId) }
}
var needsYou: Int
var working: Int
/// The thread that has waited longest for you.
var next: Next?
var hostsOffline: Int
}
extension Summary {
/// The same counts worked out from a feed. The feed lists needs-you oldest request first, so its
/// first item is the one that has waited longest.
init(_ feed: Feed) {
let next = feed.needsYou.first
self.init(
needsYou: feed.needsYou.count,
working: feed.working.count,
next: next.map { Next(hostId: $0.hostId, threadId: $0.threadId, title: $0.title, hostLabel: $0.hostLabel) },
hostsOffline: feed.hosts.filter { $0.status != .online }.count)
}
}
struct Message: Codable, Hashable, Sendable, Identifiable {
struct Role: RawRepresentable, Codable, Hashable, Sendable {
let rawValue: String
@@ -203,8 +254,22 @@ struct SendMessageRequest: Codable, Sendable {
}
enum RespondRequest: Encodable, Sendable {
/// One question's answer: an option value or typed text, or option values for a multi-select question.
enum Answer: Encodable, Hashable, Sendable {
case text(String)
case values([String])
func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .text(text): try container.encode(text)
case let .values(values): try container.encode(values)
}
}
}
case decision(requestId: String, decision: ApprovalDecision)
case answers(requestId: String, answers: [String: String])
case answers(requestId: String, answers: [String: Answer])
private enum CodingKeys: String, CodingKey { case requestId, decision, answers }