From bfcbb69550d0bd5413b6f0d6867e2157bcb7357d Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Mon, 2 Nov 2026 13:56:07 -0500 Subject: [PATCH] Watch: what changed on Recent cards and the thread screen, search in All threads, a switching complication with Allow and Deny in the Smart Stack, and question options on notifications --- watch/WristCode/API/GatewayClient.swift | 6 + watch/WristCode/API/Models.swift | 121 +++++++- watch/WristCode/API/Route.swift | 10 +- watch/WristCode/Demo/DemoGateway.swift | 54 +++- .../Notifications/NotificationHandler.swift | 117 +++++++- watch/WristCode/Shared/SharedState.swift | 9 +- watch/WristCode/Views/BrowseView.swift | 171 +++++++++++- watch/WristCode/Views/FeedView.swift | 35 +-- watch/WristCode/Views/Theme.swift | 36 ++- watch/WristCode/Views/ThreadView.swift | 48 +++- watch/WristCode/WristCodeApp.swift | 6 +- watch/WristCodeTests/ModelsTests.swift | 165 ++++++++++- watch/WristCodeTests/SnapshotTests.swift | 118 +++++++- watch/WristCodeWidget/RespondIntent.swift | 37 +++ watch/WristCodeWidget/WaitingView.swift | 264 ++++++++++++++---- watch/WristCodeWidget/WaitingWidget.swift | 37 +-- watch/project.yml | 3 +- watch/screenshots/browse-hosts.png | Bin 26708 -> 32930 bytes watch/screenshots/complication-waiting.png | Bin 0 -> 29640 bytes watch/screenshots/complication-working.png | Bin 0 -> 32131 bytes watch/screenshots/complication.png | Bin 48287 -> 61518 bytes watch/screenshots/feed-changes.png | Bin 0 -> 52258 bytes watch/screenshots/feed-recent.png | Bin 119704 -> 126039 bytes watch/screenshots/notification-question.png | Bin 0 -> 36819 bytes watch/screenshots/recent.png | Bin 154624 -> 161050 bytes watch/screenshots/search-empty.png | Bin 0 -> 17595 bytes watch/screenshots/search.png | Bin 0 -> 53476 bytes watch/screenshots/smart-stack.png | Bin 0 -> 43144 bytes watch/screenshots/thread-changes.png | Bin 0 -> 100646 bytes 29 files changed, 1100 insertions(+), 137 deletions(-) create mode 100644 watch/WristCodeWidget/RespondIntent.swift create mode 100644 watch/screenshots/complication-waiting.png create mode 100644 watch/screenshots/complication-working.png create mode 100644 watch/screenshots/feed-changes.png create mode 100644 watch/screenshots/notification-question.png create mode 100644 watch/screenshots/search-empty.png create mode 100644 watch/screenshots/search.png create mode 100644 watch/screenshots/smart-stack.png create mode 100644 watch/screenshots/thread-changes.png diff --git a/watch/WristCode/API/GatewayClient.swift b/watch/WristCode/API/GatewayClient.swift index a868338..f368c37 100644 --- a/watch/WristCode/API/GatewayClient.swift +++ b/watch/WristCode/API/GatewayClient.swift @@ -62,6 +62,8 @@ protocol GatewayAPI: Sendable { func projects() async throws -> ProjectsResponse /// A project's newest 50 threads, settled ones included. Subagents are left out. func projectThreads(hostId: String, projectId: String) async throws -> ThreadListResponse + /// Thread titles and messages on every online host matching `query`, which needs two characters. + func search(_ query: String) async throws -> SearchResponse func newThread(_ request: NewThreadRequest) async throws -> NewThreadResponse func registerPush(_ registration: PushRegistration) async throws func status() async throws -> StatusResponse @@ -137,6 +139,10 @@ struct GatewayClient: GatewayAPI { try await call("GET", ["v1", "projects", hostId, projectId, "threads"]) } + func search(_ query: String) async throws -> SearchResponse { + try await call("GET", ["v1", "search"], query: [URLQueryItem(name: "q", value: query)]) + } + func newThread(_ request: NewThreadRequest) async throws -> NewThreadResponse { try await call("POST", ["v1", "threads"], body: request) } diff --git a/watch/WristCode/API/Models.swift b/watch/WristCode/API/Models.swift index bd582de..8e2763e 100644 --- a/watch/WristCode/API/Models.swift +++ b/watch/WristCode/API/Models.swift @@ -157,6 +157,8 @@ struct FeedItem: Codable, Hashable, Sendable, Identifiable { /// What a working thread is doing right now. Nil when nothing is in progress, and from older /// gateways. var activity: Activity? = nil + /// What the latest run changed, in total. Nil when it changed no files, and from older gateways. + var changes: ChangeTotals? = nil var id: String { "\(hostId)/\(threadId)" } var route: Route { .thread(hostId: hostId, threadId: threadId) } @@ -189,6 +191,62 @@ struct Activity: Codable, Hashable, Sendable { var startedAt: Date? } +/// What a thread's latest run changed, in total, for the line on a Recent card. +struct ChangeTotals: Codable, Hashable, Sendable { + var files: Int + var additions: Int + var deletions: Int + + /// "+42", "−8" and "3 files", the pieces of the card's "+42 −8 · 3 files". + var parts: (additions: String, deletions: String, files: String) { + ("+\(additions)", "\u{2212}\(deletions)", files == 1 ? "1 file" : "\(files) files") + } +} + +/// A file the latest run changed. `name` is the last part of `path`. +struct ChangedFile: Codable, Hashable, Sendable, Identifiable { + var path: String + var name: String + var additions: Int + var deletions: Int + + var id: String { path } +} + +/// A thread search hit: the thread, and the text around the match when it was inside a message. +struct SearchHit: Codable, Hashable, Sendable, Identifiable { + var thread: ThreadRow + var projectTitle: String + var hostLabel: String + /// Nil when only the title matched. + var snippet: String? + + var id: String { thread.id } +} + +/// `GET /v1/search?q=`: hits from every online host, newest match first. +struct SearchResponse: Codable, Hashable, Sendable { + var hits: [SearchHit] + /// Labels of hosts that were offline or failed, so their threads were not searched. + var unsearched: [String] + + /// The line under the results naming the hosts left out, such as "Laptop is offline, so its + /// threads were not searched." `offline` holds the labels of hosts the watch knows are offline; + /// any other host failed or was too slow, which the gateway does not tell apart. + static func unsearchedNote(_ labels: [String], offline: Set) -> String? { + guard !labels.isEmpty else { return nil } + let names: String = switch labels.count { + case 1: labels[0] + case 2: "\(labels[0]) and \(labels[1])" + default: labels.dropLast().joined(separator: ", ") + " and " + labels[labels.count - 1] + } + guard labels.allSatisfy(offline.contains) else { return "\(names) could not be searched." } + return labels.count == 1 + ? "\(names) is offline, so its threads were not searched." + : "\(names) are offline, so their threads were not searched." + } +} + /// A thread in a list: a project's threads, or a thread's subagents. struct ThreadRow: Codable, Hashable, Sendable, Identifiable { var hostId: String @@ -277,13 +335,26 @@ struct SettleRecentResponse: Codable, Hashable, Sendable { var failed: Int } -/// `GET /v1/summary`: the counts behind the complication. +/// `GET /v1/summary`: what the complication and the Smart Stack widget show. struct Summary: Codable, Hashable, Sendable { struct Next: Codable, Hashable, Sendable { var hostId: String var threadId: String var title: String var hostLabel: String + /// The command it wants to run or its first question, trimmed. Nil until the gateway has read + /// it, and from older gateways. + var request: String? = nil + + var route: Route { .thread(hostId: hostId, threadId: threadId) } + } + + /// The working thread updated most recently, with what it is doing. + struct Current: Codable, Hashable, Sendable { + var hostId: String + var threadId: String + var title: String + var activity: Activity? var route: Route { .thread(hostId: hostId, threadId: threadId) } } @@ -292,18 +363,24 @@ struct Summary: Codable, Hashable, Sendable { var working: Int /// The thread that has waited longest for you. var next: Next? + /// Nil when nothing is working, and from older gateways. + var current: Current? = nil 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. + /// The same summary worked out from a feed. The feed lists needs-you oldest request first, so its + /// first item is the one that has waited longest, and working newest first. init(_ feed: Feed) { let next = feed.needsYou.first + let current = feed.working.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) }, + next: next.map { + Next(hostId: $0.hostId, threadId: $0.threadId, title: $0.title, hostLabel: $0.hostLabel, request: $0.request?.summary) + }, + current: current.map { Current(hostId: $0.hostId, threadId: $0.threadId, title: $0.title, activity: $0.activity) }, hostsOffline: feed.hosts.filter { $0.status != .online }.count) } } @@ -336,14 +413,16 @@ struct ThreadDetail: Codable, Hashable, Sendable { var earlierCursor: String? = nil /// Subagents and delegated threads this thread started, newest first. var subagents: [ThreadRow] = [] + /// The files the latest run changed, most lines changed first. + var changedFiles: [ChangedFile] = [] } extension ThreadDetail { private enum CodingKeys: String, CodingKey { - case item, messages, canStop, askFirst, earlierCursor, subagents + case item, messages, canStop, askFirst, earlierCursor, subagents, changedFiles } - /// `subagents` is newer than the rest, so a gateway that leaves it out lists none. + /// `subagents` and `changedFiles` are newer than the rest, so a gateway that leaves them out lists none. init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.init( @@ -352,7 +431,8 @@ extension ThreadDetail { canStop: try container.decode(Bool.self, forKey: .canStop), askFirst: try container.decodeIfPresent(Bool.self, forKey: .askFirst), earlierCursor: try container.decodeIfPresent(String.self, forKey: .earlierCursor), - subagents: try container.decodeIfPresent([ThreadRow].self, forKey: .subagents) ?? []) + subagents: try container.decodeIfPresent([ThreadRow].self, forKey: .subagents) ?? [], + changedFiles: try container.decodeIfPresent([ChangedFile].self, forKey: .changedFiles) ?? []) } } @@ -605,7 +685,7 @@ struct SendMessageRequest: Codable, Sendable { var text: String } -enum RespondRequest: Encodable, Sendable { +enum RespondRequest: Encodable, Equatable, 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) @@ -657,6 +737,31 @@ struct ErrorResponse: Codable, Sendable { var error: String } +/// `question` on an `input` push that asks one single-choice question: its options, so the +/// notification can offer each as a button. Answer with `[id: value]` on the respond route. +struct PushQuestion: Codable, Hashable, Sendable { + var id: String + /// At most 4. + var options: [PendingRequest.Question.Choice] + /// Adds a spoken or typed answer. + var allowCustomAnswer: Bool + + /// The question in a push's `userInfo`, or nil when the push has none or it does not decode. + init?(userInfo: [AnyHashable: Any]) { + guard let object = userInfo["question"], JSONSerialization.isValidJSONObject(object), + let data = try? JSONSerialization.data(withJSONObject: object), + let question = try? JSON.decoder().decode(Self.self, from: data) + else { return nil } + self = question + } + + init(id: String, options: [PendingRequest.Question.Choice], allowCustomAnswer: Bool) { + self.id = id + self.options = options + self.allowCustomAnswer = allowCustomAnswer + } +} + /// Notification categories the watch registers actions for. enum PushCategory: String, CaseIterable, Sendable { case approval, finished, failed, input, host, test diff --git a/watch/WristCode/API/Route.swift b/watch/WristCode/API/Route.swift index afb5c50..33fce0b 100644 --- a/watch/WristCode/API/Route.swift +++ b/watch/WristCode/API/Route.swift @@ -15,12 +15,14 @@ enum Route: Hashable, Sendable { case allThreads case hostProjects(hostId: String) case projectThreads(hostId: String, projectId: String) + /// Threads on every host matching a search from All threads. + case search(query: String) static let scheme = "wristcode" /// `wristcode://thread//`, `wristcode://new`, `wristcode://recent`, `wristcode://status`, /// `wristcode://host/`, `wristcode://notifications`, `wristcode://threads`, - /// `wristcode://projects/` or `wristcode://project//`. + /// `wristcode://projects/`, `wristcode://project//` or `wristcode://search?q=`. var url: URL { var components = URLComponents() components.scheme = Self.scheme @@ -50,6 +52,9 @@ enum Route: Hashable, Sendable { case let .projectThreads(hostId, projectId): components.host = "project" components.percentEncodedPath = path(hostId, projectId) + case let .search(query): + components.host = "search" + components.queryItems = [URLQueryItem(name: "q", value: query)] } return components.url! } @@ -79,6 +84,9 @@ enum Route: Hashable, Sendable { self = .hostProjects(hostId: parts[0]) case "project" where parts.count == 2 && parts.allSatisfy { !$0.isEmpty }: self = .projectThreads(hostId: parts[0], projectId: parts[1]) + case "search": + guard let query = components.queryItems?.first(where: { $0.name == "q" })?.value, !query.isEmpty else { return nil } + self = .search(query: query) default: return nil } diff --git a/watch/WristCode/Demo/DemoGateway.swift b/watch/WristCode/Demo/DemoGateway.swift index 757440f..0769607 100644 --- a/watch/WristCode/Demo/DemoGateway.swift +++ b/watch/WristCode/Demo/DemoGateway.swift @@ -2,7 +2,8 @@ // SwiftUI previews use it, so every screen renders without a live gateway. It reacts to writes: // answering an approval moves the thread to working, a reply restarts a finished thread, settling // hides a thread from the feed, Ask before acting and the notification switches stick, and so on. -// Subagent threads open like any other but stay out of the feed, as the gateway's do. +// Subagent threads open like any other but stay out of the feed, as the gateway's do. Search matches +// titles and messages on the online hosts. import Foundation @@ -13,6 +14,8 @@ actor DemoGateway: GatewayAPI { private var messages: [String: [Message]] /// Pages before each thread's messages, newest page first. private let earlierPages: [String: [[Message]]] + /// Files each thread's latest run changed, by thread id. + private let changedFiles: [String: [ChangedFile]] /// Subagent ids by parent id. Subagents are in `items` but not the feed. private let children: [String: [String]] private let subagentIds: Set @@ -30,6 +33,7 @@ actor DemoGateway: GatewayAPI { items = fixtures.items messages = fixtures.messages earlierPages = fixtures.history + changedFiles = fixtures.changedFiles children = fixtures.children subagentIds = Set(fixtures.children.values.joined()) settled = fixtures.settled @@ -52,7 +56,8 @@ actor DemoGateway: GatewayAPI { item: item, messages: messages[item.id] ?? [], canStop: item.phase.isWorking, askFirst: askingFirst.contains(item.id), earlierCursor: earlierPages[item.id] == nil ? nil : "p0", - subagents: (children[item.id] ?? []).compactMap { id in items.first { $0.id == id }.map(row) }) + subagents: (children[item.id] ?? []).compactMap { id in items.first { $0.id == id }.map(row) }, + changedFiles: changedFiles[item.id] ?? []) } /// Cursors are `p`, the n-th page back from the thread's messages. @@ -149,6 +154,31 @@ actor DemoGateway: GatewayAPI { return ThreadListResponse(items: rows) } + /// Titles first, then messages, on online hosts only, newest first. A message match carries the + /// words around it. + func search(_ query: String) async throws -> SearchResponse { + let query = query.trimmingCharacters(in: .whitespacesAndNewlines) + guard query.count >= 2 else { throw GatewayError.http(status: 400, message: "Search for at least two letters.") } + let online = Set(hosts.filter { $0.status == .online }.map(\.id)) + let hits: [(SearchHit, Date)] = items.compactMap { item in + guard online.contains(item.hostId) else { return nil } + let hit = { (snippet: String?) in + SearchHit(thread: self.row(item), projectTitle: item.projectTitle, hostLabel: item.hostLabel, snippet: snippet) + } + if item.title.localizedCaseInsensitiveContains(query) { return (hit(nil), item.updatedAt) } + guard let message = messages[item.id]?.last(where: { $0.text.localizedCaseInsensitiveContains(query) }), + let range = message.text.range(of: query, options: .caseInsensitive) + else { return nil } + let start = message.text.index(range.lowerBound, offsetBy: -30, limitedBy: message.text.startIndex) ?? message.text.startIndex + let end = message.text.index(range.upperBound, offsetBy: 50, limitedBy: message.text.endIndex) ?? message.text.endIndex + let snippet = (start > message.text.startIndex ? "…" : "") + message.text[start ..< end] + (end < message.text.endIndex ? "…" : "") + return (hit(snippet), message.createdAt) + } + return SearchResponse( + hits: hits.sorted { $0.1 > $1.1 }.map(\.0), + unsearched: hosts.filter { !online.contains($0.id) }.map(\.label)) + } + func newThread(_ request: NewThreadRequest) async throws -> NewThreadResponse { guard let host = projectHosts.first(where: { $0.id == request.hostId }), let project = host.projects.first(where: { $0.id == request.projectId }) @@ -229,6 +259,8 @@ struct DemoFixtures { let items: [FeedItem] let messages: [String: [Message]] let history: [String: [[Message]]] + /// Files each thread's latest run changed, by thread id. Each thread's `changes` totals them. + let changedFiles: [String: [ChangedFile]] /// Subagent ids by parent id. let children: [String: [String]] /// Threads settled from the start, which only a project's thread list shows. @@ -388,8 +420,22 @@ struct DemoFixtures { } settled = Set(settledThreads.map(\.id)) - items = [approval, question, release, homelab, scaffold, containerfile, statusScreens, nextjs, hyprlock, cdn] - + olderRecent + settledThreads + [path, image, healthcheck] + func file(_ path: String, _ additions: Int, _ deletions: Int) -> ChangedFile { + ChangedFile(path: path, name: String(path.split(separator: "/").last ?? ""), additions: additions, deletions: deletions) + } + let changed = [ + nextjs.id: [file("package.json", 31, 0), file("next.config.ts", 9, 6), file("app/layout.tsx", 2, 2)], + cdn.id: [file("src/headers.rs", 14, 6)], + ] + changedFiles = changed + items = ([approval, question, release, homelab, scaffold, containerfile, statusScreens, nextjs, hyprlock, cdn] + + olderRecent + settledThreads + [path, image, healthcheck]).map { item in + guard let files = changed[item.id] else { return item } + var item = item + item.changes = ChangeTotals( + files: files.count, additions: files.map(\.additions).reduce(0, +), deletions: files.map(\.deletions).reduce(0, +)) + return item + } children = [homelab.id: [path.id], containerfile.id: [image.id, healthcheck.id]] func m(_ id: String, _ role: Message.Role, _ minutes: Double, _ text: String) -> Message { diff --git a/watch/WristCode/Notifications/NotificationHandler.swift b/watch/WristCode/Notifications/NotificationHandler.swift index dfdc5cf..dbcf25c 100644 --- a/watch/WristCode/Notifications/NotificationHandler.swift +++ b/watch/WristCode/Notifications/NotificationHandler.swift @@ -1,12 +1,22 @@ -// Notification categories and actions from docs/api.md, and what each action does. Allow, Deny -// and Reply run in the background against the gateway; tapping a notification opens its thread. +// Notification categories and actions from docs/api.md, and what each action does. Allow, Deny, +// Reply and a question's answers run in the background against the gateway; tapping a notification +// opens its thread. +// +// An `input` push that carries `question` gets one button per option, plus "Say an answer" when the +// question takes its own answer. watchOS only lets a notification's actions change per push from +// its hosting controller, so `InputNotificationController` sets them when the push arrives. Any other +// `input` push keeps the category's Answer and Open, which open the thread's question screen. import Foundation +import SwiftUI import UserNotifications +import WatchKit /// What a notification response asks for, parsed out of the payload. enum NotificationCommand: Equatable, Sendable { case respond(hostId: String, threadId: String, requestId: String, decision: ApprovalDecision) + /// One answer to a push's single question: an option's value, or what was said or typed. + case answer(hostId: String, threadId: String, requestId: String, questionId: String, value: String) case reply(hostId: String, threadId: String, text: String) case open(Route?) case none @@ -16,9 +26,16 @@ enum NotificationCommand: Equatable, Sendable { static let deny = "deny" static let reply = "reply" static let open = "open" + /// Opens the thread, on a question the notification cannot answer itself. + static let answer = "answer" + /// A spoken or typed answer to a push's question. + static let say = "say" + /// `option.` picks the push question's n-th option. + static let optionPrefix = "option." } - /// `userInfo` holds the custom keys `hostId`, `threadId` and, for approvals, `requestId`. + /// `userInfo` holds the custom keys `hostId`, `threadId` and, for approvals and questions, + /// `requestId`, and for a lone single-choice question, `question`. init(actionIdentifier: String, userInfo: [AnyHashable: Any], text: String?) { // The gateway sends `threadId: ""` on host notifications, so empty means absent. func value(_ key: String) -> String? { @@ -29,6 +46,14 @@ enum NotificationCommand: Equatable, Sendable { let requestId = value("requestId") let thread: Route? = if let hostId, let threadId { .thread(hostId: hostId, threadId: threadId) } else { nil } + /// The question's answer, or the thread when the answer or the keys it needs are missing. + func answer(_ value: String?) -> Self { + let value = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard let hostId, let threadId, let requestId, let question = PushQuestion(userInfo: userInfo), !value.isEmpty + else { return .open(thread) } + return .answer(hostId: hostId, threadId: threadId, requestId: requestId, questionId: question.id, value: value) + } + switch actionIdentifier { case Action.allow, Action.deny: guard let hostId, let threadId, let requestId else { self = .open(thread); return } @@ -39,13 +64,31 @@ enum NotificationCommand: Equatable, Sendable { let text = text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard let hostId, let threadId, !text.isEmpty else { self = .open(thread); return } self = .reply(hostId: hostId, threadId: threadId, text: text) + case Action.say: + self = answer(text) + case let id where id.hasPrefix(Action.optionPrefix): + let index = Int(id.dropFirst(Action.optionPrefix.count)) + let options = PushQuestion(userInfo: userInfo)?.options ?? [] + self = answer(index.flatMap { options.indices.contains($0) ? options[$0].value : nil }) case UNNotificationDismissActionIdentifier: self = .none default: - // The Open action, or a tap on the notification itself. + // Open, Answer, or a tap on the notification itself. self = .open(thread) } } + + /// The respond route's thread and body, for Allow, Deny and a question's answer. + var response: (hostId: String, threadId: String, request: RespondRequest)? { + switch self { + case let .respond(hostId, threadId, requestId, decision): + (hostId, threadId, .decision(requestId: requestId, decision: decision)) + case let .answer(hostId, threadId, requestId, questionId, value): + (hostId, threadId, .answers(requestId: requestId, answers: [questionId: .text(value)])) + default: + nil + } + } } @MainActor @@ -66,13 +109,15 @@ final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { let reply = UNTextInputNotificationAction( identifier: Action.reply, title: "Reply", options: [], textInputButtonTitle: "Send", textInputPlaceholder: "Reply") + let answer = UNNotificationAction(identifier: Action.answer, title: "Answer", options: [.foreground]) func category(_ id: PushCategory, _ actions: [UNNotificationAction]) -> UNNotificationCategory { UNNotificationCategory(identifier: id.rawValue, actions: actions, intentIdentifiers: [], options: []) } return [ category(.approval, [allow, deny]), - category(.input, [open]), + // A question push replaces these with its options; see `inputActions`. + category(.input, [answer, open]), category(.finished, [reply, open]), category(.failed, [open]), category(.host, []), @@ -80,6 +125,24 @@ final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { ] } + /// An `input` push's actions: its question's options, "Say an answer" when the question takes + /// one, and Open. Without a question, Answer and Open, as the category registers them. + static func inputActions(_ question: PushQuestion?) -> [UNNotificationAction] { + typealias Action = NotificationCommand.Action + let open = UNNotificationAction(identifier: Action.open, title: "Open", options: [.foreground]) + guard let question else { + return [UNNotificationAction(identifier: Action.answer, title: "Answer", options: [.foreground]), open] + } + let options = question.options.enumerated().map { index, option in + UNNotificationAction(identifier: Action.optionPrefix + String(index), title: option.label, options: []) + } + let say = UNTextInputNotificationAction( + identifier: Action.say, title: "Say an answer", options: [], + icon: UNNotificationActionIcon(systemImageName: "mic.fill"), + textInputButtonTitle: "Send", textInputPlaceholder: "Answer") + return options + (question.allowCustomAnswer ? [say] : []) + [open] + } + nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification ) async -> UNNotificationPresentationOptions { @@ -107,12 +170,10 @@ final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { model.reloadComplication() await model.refreshFeed() return - case let .respond(hostId, threadId, requestId, decision): - guard model.mode != .unpaired else { return } + case .respond, .answer: + guard model.mode != .unpaired, let response = command.response else { return } await attempt("Couldn't answer") { - try await model.api.respond( - hostId: hostId, threadId: threadId, - request: .decision(requestId: requestId, decision: decision)) + try await model.api.respond(hostId: response.hostId, threadId: response.threadId, request: response.request) } case let .reply(hostId, threadId, text): guard model.mode != .unpaired else { return } @@ -141,3 +202,39 @@ final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { } } } + +/// The long look for `input` pushes. It shows the push's title and question, and sets the push's +/// own actions, which is the one place watchOS takes per-push action titles. +final class InputNotificationController: WKUserNotificationHostingController { + private var threadTitle = "" + private var questionText = "" + + override var body: InputNotificationView { + InputNotificationView(title: threadTitle, question: questionText) + } + + override func didReceive(_ notification: UNNotification) { + let content = notification.request.content + threadTitle = content.title + questionText = content.body + notificationActions = NotificationHandler.inputActions(PushQuestion(userInfo: content.userInfo)) + } +} + +/// The thread's title and its question, above the actions the system draws. +struct InputNotificationView: View { + var title: String + var question: String + + var body: some View { + VStack(alignment: .leading, spacing: 3) { + Text(title) + .font(.system(size: 15, weight: .semibold)) + Text(question) + .font(.system(size: 13)) + .foregroundStyle(Theme.secondaryText) + } + .frame(maxWidth: .infinity, alignment: .leading) + .fontDesign(.rounded) + } +} diff --git a/watch/WristCode/Shared/SharedState.swift b/watch/WristCode/Shared/SharedState.swift index 8cd8c82..c4a89a8 100644 --- a/watch/WristCode/Shared/SharedState.swift +++ b/watch/WristCode/Shared/SharedState.swift @@ -1,6 +1,7 @@ // What the app tells the complication besides the credentials: whether it is in demo mode, and the // last summary it saw, which the complication shows when it cannot reach the gateway. Both live in -// the shared Keychain group next to the device token, the one store the two processes share. +// the shared Keychain group next to the device token, the one store the two processes share. `api` +// is the gateway the complication and its Allow and Deny buttons call. import Foundation @@ -14,6 +15,12 @@ enum SharedState { set { DeviceStore.setSharedData(newValue ? Data([1]) : nil, account: demoAccount) } } + /// The demo gateway in demo mode, the paired gateway otherwise, and nil when not paired. + static var api: (any GatewayAPI)? { + if demo { return DemoGateway.shared } + return DeviceStore.load().map { GatewayClient(baseURL: $0.gatewayURL, token: $0.token) } + } + /// The last summary the app or the complication saw, or nil after signing out. static var lastSummary: Summary? { get { DeviceStore.sharedData(summaryAccount).flatMap { try? JSON.decoder().decode(Summary.self, from: $0) } } diff --git a/watch/WristCode/Views/BrowseView.swift b/watch/WristCode/Views/BrowseView.swift index 011aedf..584fbf1 100644 --- a/watch/WristCode/Views/BrowseView.swift +++ b/watch/WristCode/Views/BrowseView.swift @@ -1,11 +1,12 @@ -// All threads, opened from the last row of the feed: hosts, then a host's projects sorted by title, -// then a project's threads with settled ones included. Hosts and projects come from -// `GET /v1/projects`, threads from `GET /v1/projects/:hostId/:projectId/threads`. Each screen loads -// when it appears; nothing polls. A thread opens the usual thread screen. +// All threads, opened from the last row of the feed: a search field over the hosts, then a host's +// projects sorted by title, then a project's threads with settled ones included. Hosts and projects +// come from `GET /v1/projects`, threads from `GET /v1/projects/:hostId/:projectId/threads`, and +// search results from `GET /v1/search`. Each screen loads when it appears; nothing polls. A thread or +// a search hit opens the usual thread screen. import SwiftUI -/// Every host, offline ones dimmed. Each opens its projects. +/// A search field, then every host, offline ones dimmed. Each host opens its projects. struct HostsView: View { @Environment(AppModel.self) private var model /// Snapshots render the rows without their List, which ImageRenderer leaves blank. @@ -13,6 +14,9 @@ struct HostsView: View { var body: some View { BrowseList(scrolls: scrolls, error: model.projectsError, loaded: model.projects != nil, reload: model.loadProjects) { + SearchField() + .feedRow() + } rows: { if let hosts = model.projects?.hosts { ForEach(hosts) { host in BrowseRow( @@ -105,6 +109,144 @@ struct ProjectThreadsView: View { } } +/// Threads on every host matching `query`: each hit's host and project, its title, and the words +/// around the match when it was inside a message. Says which hosts were left out. +struct SearchView: View { + @Environment(AppModel.self) private var model + let query: String + @State private var response: SearchResponse? + @State private var error: String? + private let scrolls: Bool + + /// `response` starts the screen loaded, for snapshots. + init(query: String, response: SearchResponse? = nil, scrolls: Bool = true) { + self.query = query + self.scrolls = scrolls + _response = State(initialValue: response) + } + + var body: some View { + BrowseList(scrolls: scrolls, error: error, loaded: response != nil, reload: load) { + if let response { + ForEach(response.hits) { hit in + SearchHitCard(hit: hit, query: query) + .feedRow() + } + if response.hits.isEmpty { + Text("No threads match.") + .font(.system(size: 13)) + .foregroundStyle(Theme.secondaryText) + .frame(maxWidth: .infinity) + .padding(.top, 16) + .feedRow() + } + if let note = SearchResponse.unsearchedNote(response.unsearched, offline: offlineHosts) { + Text(note) + .font(.system(size: 11)) + .foregroundStyle(Theme.secondaryText) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.horizontal, 8) + .feedRow() + } + } + } + .navigationTitle("\u{201C}\(query)\u{201D}") + } + + /// Labels of hosts the feed or All threads last saw offline. + private var offlineHosts: Set { + let feed = model.feed?.hosts.filter { $0.status != .online }.map(\.label) ?? [] + let browsed = model.projects?.hosts.filter { $0.status != .online }.map(\.label) ?? [] + return Set(feed + browsed) + } + + private func load() async { + do { + response = try await model.api.search(query) + error = nil + } catch { + self.error = model.message(for: error) + } + } +} + +/// The field at the top of All threads. Tapping it opens dictation or the keyboard, as any watchOS +/// text field does, and submitting opens the results. +private struct SearchField: View { + @Environment(AppModel.self) private var model + + var body: some View { + TextFieldLink(prompt: Text("Search threads")) { + HStack(spacing: 7) { + Image(systemName: "magnifyingglass") + .foregroundStyle(Theme.secondaryText) + Text("Search") + .foregroundStyle(Theme.secondaryText) + Spacer(minLength: 4) + Image(systemName: "mic.fill") + .foregroundStyle(Theme.work) + } + .font(.system(size: 14, weight: .semibold)) + .padding(.vertical, 9) + .padding(.horizontal, 12) + .background(Theme.control.opacity(0.7), in: RoundedRectangle(cornerRadius: 18, style: .continuous)) + .contentShape(Rectangle()) + } onSubmit: { text in + let query = text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return } + model.path.append(.search(query: query)) + } + .buttonStyle(.plain) + .accessibilityLabel("Search threads") + } +} + +/// A search hit: "host · project", the title, dimmed once finished or settled, and the snippet with +/// the matched words picked out. Opens the thread. +private struct SearchHitCard: View { + @Environment(AppModel.self) private var model + var hit: SearchHit + var query: String + + var body: some View { + Button { model.path.append(hit.thread.route) } label: { + Card(edge: Theme.card) { + Text("\(hit.hostLabel) · \(hit.projectTitle)") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Theme.secondaryText) + .lineLimit(1) + Text(hit.thread.title) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(hit.thread.isActive ? Color.white : Theme.secondaryText) + .lineLimit(2) + .padding(.top, 1) + if let snippet = hit.snippet { + Text(highlighted(snippet)) + .font(.system(size: 12)) + .foregroundStyle(Theme.secondaryText) + .lineLimit(3) + .padding(.top, 3) + } + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + /// The snippet with each match of the query bold and white. + private func highlighted(_ snippet: String) -> AttributedString { + var text = AttributedString(snippet) + var searchFrom = text.startIndex + while let range = text[searchFrom...].range(of: query, options: .caseInsensitive) { + text[range].inlinePresentationIntent = .stronglyEmphasized + text[range].foregroundColor = .white + searchFrom = range.upperBound + } + return text + } +} + /// A thread on one line: status dot, title, and `trailing` on the right. Opens the thread. struct ThreadLine: View { @Environment(AppModel.self) private var model @@ -135,11 +277,13 @@ struct ThreadLine: View { } /// The List each browse screen scrolls, with loading, an error and Try again. Loads on appear. -private struct BrowseList: View { +/// `header` shows above the rows even while they load. +private struct BrowseList: View { var scrolls: Bool var error: String? var loaded: Bool var reload: () async -> Void + @ViewBuilder var header: Header @ViewBuilder var rows: Rows var body: some View { @@ -155,6 +299,7 @@ private struct BrowseList: View { } @ViewBuilder private var content: some View { + header if loaded { rows } else if error == nil { @@ -174,6 +319,12 @@ private struct BrowseList: View { } } +extension BrowseList where Header == EmptyView { + init(scrolls: Bool, error: String?, loaded: Bool, reload: @escaping () async -> Void, @ViewBuilder rows: () -> Rows) { + self.init(scrolls: scrolls, error: error, loaded: loaded, reload: reload, header: { EmptyView() }, rows: rows) + } +} + /// A plain card with a title, an optional line under it, and a chevron. private struct BrowseRow: View { @Environment(AppModel.self) private var model @@ -228,6 +379,14 @@ private struct BrowseNote: View { .fontDesign(.rounded) } +#Preview("Search") { + NavigationStack { + SearchView(query: "containerfile") + } + .environment(AppModel.preview()) + .fontDesign(.rounded) +} + #Preview("Threads") { NavigationStack { ProjectThreadsView(hostId: "mac-mini", projectId: "p-wc-mac") diff --git a/watch/WristCode/Views/FeedView.swift b/watch/WristCode/Views/FeedView.swift index 17dc078..dba9fc3 100644 --- a/watch/WristCode/Views/FeedView.swift +++ b/watch/WristCode/Views/FeedView.swift @@ -317,8 +317,8 @@ struct NeedsYouCard: View { } /// A teal card for a working thread, or a grey one for a recent thread. A working thread shows the -/// step it is on, or its latest message when the gateway names no step. A recent thread that failed -/// to settle shows why. +/// step it is on, or its latest message when the gateway names no step. A recent thread shows what +/// its latest run changed, such as "+42 −8 · 3 files", and why it failed to settle. struct StatusCard: View { @Environment(AppModel.self) private var model var item: FeedItem @@ -344,6 +344,12 @@ struct StatusCard: View { .foregroundStyle(Theme.secondaryText) .lineLimit(2) .padding(.top, 3) + } else if !item.phase.isWorking, let changes = item.changes { + Text(changes: changes) + .font(.system(size: 11)) + .foregroundStyle(Theme.secondaryText) + .lineLimit(1) + .padding(.top, 3) } if let error = model.settleErrors[item.id] { Text(error) @@ -374,7 +380,7 @@ struct ActivityLine: View { var body: some View { HStack(alignment: .firstTextBaseline, spacing: 4) { - Image(systemName: symbol) + Image(systemName: activity.kind.symbol) .font(.system(size: 10, weight: .bold)) .foregroundStyle(Theme.work) .frame(width: 14) @@ -393,30 +399,13 @@ struct ActivityLine: View { } private var label: Text { - // Marked as code, which Text sets in monospace. A monospaced Font loses to the app's rounded design. - let mono = { (text: String) -> Text in - var code = AttributedString(text) - code.inlinePresentationIntent = .code - return Text(code).font(.system(size: 11)) - } + let mono = Text(code: activity.text).font(.system(size: 11)) return switch activity.kind { - case .command: mono(activity.text) - case .edit: Text("Editing \(mono(activity.text))") + case .command: mono + case .edit: Text("Editing \(mono)") default: Text(activity.text) } } - - private var symbol: String { - switch activity.kind { - case .command: "terminal" - case .edit: "pencil" - case .search: "magnifyingglass" - case .web: "globe" - case .subagent: "arrow.triangle.branch" - case .thinking: "ellipsis" - default: "wrench.adjustable" - } - } } #Preview("Feed") { diff --git a/watch/WristCode/Views/Theme.swift b/watch/WristCode/Views/Theme.swift index 2e1c957..6006db3 100644 --- a/watch/WristCode/Views/Theme.swift +++ b/watch/WristCode/Views/Theme.swift @@ -1,5 +1,5 @@ // Mock B colors and the shared card. Amber means it needs you, teal means working, grey means done, -// and red means broken. +// and red means broken. Green and red also count lines added and removed. import SwiftUI @@ -12,6 +12,40 @@ enum Theme { static let secondaryText = Color(red: 0.682, green: 0.682, blue: 0.698) // #aeaeb2 static let bodyText = Color(red: 0.898, green: 0.898, blue: 0.918) // #e5e5ea static let bad = Color(red: 1.0, green: 0.420, blue: 0.369) // #ff6b5e + static let add = Color(red: 0.353, green: 0.820, blue: 0.478) // #5ad17a +} + +extension Text { + /// Text marked as inline code, which Text sets in monospace. A monospaced Font loses to the app's + /// rounded design. + init(code: String) { + var attributed = AttributedString(code) + attributed.inlinePresentationIntent = .code + self.init(attributed) + } + + /// "+42 −8 · 3 files" in monospace, the additions green, the deletions red and the rest in the + /// current style. Without `files`, just "+42 −8", as on a changed file's row. + init(changes: ChangeTotals, files: Bool = true) { + let parts = changes.parts + let counts = Text("\(Text(code: parts.additions).foregroundStyle(Theme.add))\(Text(code: " "))\(Text(code: parts.deletions).foregroundStyle(Theme.bad))") + self = files ? Text("\(counts)\(Text(code: " · \(parts.files)"))") : counts + } +} + +extension Activity.Kind { + /// The symbol in front of an activity line. + var symbol: String { + switch self { + case .command: "terminal" + case .edit: "pencil" + case .search: "magnifyingglass" + case .web: "globe" + case .subagent: "arrow.triangle.branch" + case .thinking: "ellipsis" + default: "wrench.adjustable" + } + } } extension Phase { diff --git a/watch/WristCode/Views/ThreadView.swift b/watch/WristCode/Views/ThreadView.swift index 5eba2bb..c3b7d07 100644 --- a/watch/WristCode/Views/ThreadView.swift +++ b/watch/WristCode/Views/ThreadView.swift @@ -1,5 +1,6 @@ // One thread: the agent's latest message large, older messages one scroll up with Load earlier above -// them, the pending approval or question with its options, the thread's subagents, Reply by +// them, the pending approval or question with its options, the thread's subagents, the files its +// latest run changed with their line counts, Reply by // dictation or Scribble, Settle once it finished or failed, an Ask before acting switch, and Stop // while a run is active. A subagent's request shows first, labeled with the subagent. Message text // renders as markdown. @@ -143,11 +144,7 @@ struct ThreadView: View { } if !detail.subagents.isEmpty { - Text("Subagents") - .font(.system(size: 11, weight: .semibold)) - .foregroundStyle(Theme.secondaryText) - .padding(.horizontal, 6) - .padding(.top, 4) + sectionLabel("Subagents") ForEach(detail.subagents) { row in ThreadLine(row: row) { Text(row.statusWord) @@ -156,6 +153,13 @@ struct ThreadView: View { } } + if !detail.changedFiles.isEmpty { + sectionLabel("Changed") + ForEach(detail.changedFiles) { file in + ChangedFileRow(file: file) + } + } + if item.phase == .completed && item.request == nil { QuickReplies { send($0) } .disabled(sending) @@ -196,6 +200,15 @@ struct ThreadView: View { } } + /// A small grey title over a list, such as Subagents or Changed. + private func sectionLabel(_ title: String) -> some View { + Text(title) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Theme.secondaryText) + .padding(.horizontal, 6) + .padding(.top, 4) + } + @ViewBuilder private func requestCards(_ request: PendingRequest, from via: FeedItem.Via?) -> some View { if request.isQuestion { @@ -517,6 +530,29 @@ private struct MessageRow: View { } } +/// A file the latest run changed: its name in monospace, and its line counts on the right. Read only. +private struct ChangedFileRow: View { + var file: ChangedFile + + var body: some View { + Card(edge: Theme.card) { + HStack(spacing: 6) { + Text(code: file.name) + .foregroundStyle(.white) + .lineLimit(1) + .truncationMode(.middle) + Spacer(minLength: 4) + Text(changes: ChangeTotals(files: 1, additions: file.additions, deletions: file.deletions), files: false) + .lineLimit(1) + .layoutPriority(1) + } + .font(.system(size: 12)) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(file.name), \(file.additions) added, \(file.deletions) removed") + } +} + /// One-tap replies for a finished turn, like mock B's chips. private struct QuickReplies: View { var send: (String) -> Void diff --git a/watch/WristCode/WristCodeApp.swift b/watch/WristCode/WristCodeApp.swift index 0c988fc..9c3f6fe 100644 --- a/watch/WristCode/WristCodeApp.swift +++ b/watch/WristCode/WristCodeApp.swift @@ -1,4 +1,5 @@ -// App entry. Notification setup and APNs registration go through the WatchKit app delegate. +// App entry. Notification setup and APNs registration go through the WatchKit app delegate, and +// `input` pushes get their own long look, which sets a question's options as its actions. import SwiftUI import WatchKit @@ -13,6 +14,7 @@ struct WristCodeApp: App { RootView() .environment(model) } + WKNotificationScene(controller: InputNotificationController.self, category: PushCategory.input.rawValue) } } @@ -47,6 +49,8 @@ struct RootView: View { ProjectsView(hostId: hostId) case let .projectThreads(hostId, projectId): ProjectThreadsView(hostId: hostId, projectId: projectId) + case let .search(query): + SearchView(query: query) } } } diff --git a/watch/WristCodeTests/ModelsTests.swift b/watch/WristCodeTests/ModelsTests.swift index 7c13162..fabe230 100644 --- a/watch/WristCodeTests/ModelsTests.swift +++ b/watch/WristCodeTests/ModelsTests.swift @@ -4,6 +4,7 @@ import Foundation import Security import Testing +import UserNotifications @testable import WristCode private func decode(_ type: T.Type, _ json: String) throws -> T { @@ -243,6 +244,65 @@ private let feedItemJSON = """ #expect(list.items.first?.route == .thread(hostId: "h", threadId: "t")) } + @Test func phase4FromDocs() throws { + let item = try decode(FeedItem.self, feedItemJSON.replacingOccurrences( + of: #""request": {"#, with: #""changes": { "files": 3, "additions": 42, "deletions": 8 }, "request": {"#)) + #expect(item.changes == ChangeTotals(files: 3, additions: 42, deletions: 8)) + #expect(try decode(FeedItem.self, feedItemJSON).changes == nil) + + let detail = try decode(ThreadDetail.self, """ + { "item": \(feedItemJSON), "messages": [], "canStop": false, "subagents": [], + "changedFiles": [{ "path": "gateway/Containerfile", "name": "Containerfile", "additions": 31, "deletions": 0 }] } + """) + #expect(detail.changedFiles == [ChangedFile(path: "gateway/Containerfile", name: "Containerfile", additions: 31, deletions: 0)]) + #expect(try decode(ThreadDetail.self, #"{ "item": \#(feedItemJSON), "messages": [], "canStop": true }"#).changedFiles.isEmpty) + + let summary = try decode(Summary.self, """ + { "needsYou": 1, "working": 3, "hostsOffline": 0, + "next": { "hostId": "h", "threadId": "t", "title": "Receipt OCR", "hostLabel": "Mac mini", "request": "bun test --filter ocr" }, + "current": { "hostId": "h", "threadId": "w", "title": "Phase 3 screens", + "activity": { "kind": "command", "text": "bun test", "startedAt": null } } } + """) + #expect(summary.next?.request == "bun test --filter ocr") + #expect(summary.current?.title == "Phase 3 screens") + #expect(summary.current?.activity?.text == "bun test") + #expect(summary.current?.route == .thread(hostId: "h", threadId: "w")) + let unread = try decode(Summary.self, """ + { "needsYou": 1, "working": 0, "hostsOffline": 0, "current": null, + "next": { "hostId": "h", "threadId": "t", "title": "Receipt OCR", "hostLabel": "Mac mini", "request": null } } + """) + #expect(unread.next?.request == nil) + #expect(unread.current == nil) + + let search = try decode(SearchResponse.self, """ + { "hits": [{ "thread": { "hostId": "h", "threadId": "t", "title": "Jellyfin image", "phase": null, "settled": true, + "updatedAt": "2026-11-01T09:41:00.000Z" }, + "projectTitle": "homelab", "hostLabel": "Mac mini", "snippet": "…rebuild from the Containerfile and…" }, + { "thread": { "hostId": "h", "threadId": "u", "title": "Gateway Containerfile", "phase": "running", "settled": false, + "updatedAt": "2026-11-01T09:42:00.000Z" }, + "projectTitle": "wristcode", "hostLabel": "Mac mini", "snippet": null }], + "unsearched": ["Laptop"] } + """) + #expect(search.hits.map(\.id) == ["h/t", "h/u"]) + #expect(search.hits.first?.snippet == "…rebuild from the Containerfile and…") + #expect(search.hits.last?.snippet == nil) + #expect(search.unsearched == ["Laptop"]) + } + + @Test func pushQuestion() throws { + let info: [AnyHashable: Any] = [ + "hostId": "h", "threadId": "t", "requestId": "r", + "question": ["id": "q1", "options": [["label": "bun:1-alpine", "value": "alpine"]], "allowCustomAnswer": true], + ] + let question = try #require(PushQuestion(userInfo: info)) + #expect(question.id == "q1") + #expect(question.options.map(\.value) == ["alpine"]) + #expect(question.allowCustomAnswer) + // Pushes without one, as from an older gateway, and malformed ones have none. + #expect(PushQuestion(userInfo: ["hostId": "h"]) == nil) + #expect(PushQuestion(userInfo: ["question": ["id": "q1"]]) == nil) + } + @Test func smallResponses() throws { #expect(try decode(PairResponse.self, #"{ "deviceId": "9b0c", "deviceToken": "wc_x" }"#).deviceToken == "wc_x") #expect(try decode(NewThreadResponse.self, #"{ "hostId": "h", "threadId": "t" }"#).threadId == "t") @@ -444,6 +504,45 @@ private let feedItemJSON = """ #expect(Summary(Feed(needsYou: [], working: [], recent: [], hosts: [])).next == nil) } + @Test func summaryFromFeedNamesRequestAndCurrent() throws { + let feed = DemoFixtures.previewFeed() + let summary = Summary(feed) + #expect(summary.next?.request == feed.needsYou.first?.request?.summary) + #expect(summary.current?.threadId == feed.working.first?.threadId) + #expect(summary.current?.activity == feed.working.first?.activity) + } + + @Test func changesLine() { + let parts = ChangeTotals(files: 3, additions: 42, deletions: 8).parts + #expect(parts.additions == "+42") + #expect(parts.deletions == "\u{2212}8") + #expect(parts.files == "3 files") + #expect(ChangeTotals(files: 1, additions: 0, deletions: 2).parts == ("+0", "\u{2212}2", "1 file")) + } + + @Test func unsearchedNote() { + #expect(SearchResponse.unsearchedNote([], offline: []) == nil) + #expect(SearchResponse.unsearchedNote(["Laptop"], offline: ["Laptop"]) + == "Laptop is offline, so its threads were not searched.") + #expect(SearchResponse.unsearchedNote(["Laptop", "LittleMan"], offline: ["Laptop", "LittleMan"]) + == "Laptop and LittleMan are offline, so their threads were not searched.") + // A host the watch saw online failed or was slow, which the gateway does not tell apart. + #expect(SearchResponse.unsearchedNote(["Laptop"], offline: []) == "Laptop could not be searched.") + #expect(SearchResponse.unsearchedNote(["Laptop", "LittleMan"], offline: ["Laptop"]) + == "Laptop and LittleMan could not be searched.") + #expect(SearchResponse.unsearchedNote(["A", "B", "C"], offline: []) == "A, B and C could not be searched.") + } + + @Test func complicationMode() { + let next = Summary.Next(hostId: "h", threadId: "t", title: "Receipt OCR", hostLabel: "Mac mini", request: "bun test") + let current = Summary.Current(hostId: "h", threadId: "w", title: "Phase 3", activity: nil) + #expect(ComplicationMode(Summary(needsYou: 2, working: 1, next: next, current: current, hostsOffline: 0)) == .waiting(next)) + #expect(ComplicationMode(Summary(needsYou: 0, working: 1, next: nil, current: current, hostsOffline: 0)) == .working(current)) + #expect(ComplicationMode(Summary(needsYou: 0, working: 0, next: nil, hostsOffline: 0)) == .idle) + // An older gateway names no working thread. + #expect(ComplicationMode(Summary(needsYou: 0, working: 3, next: nil, hostsOffline: 0)) == .idle) + } + @Test func routeURLs() { let thread = Route.thread(hostId: "mac mini", threadId: "t/1") #expect(thread.url.absoluteString == "wristcode://thread/mac%20mini/t%2F1") @@ -451,7 +550,7 @@ private let feedItemJSON = """ #expect(Route(url: Route.newThread.url) == .newThread) for route in [ Route.recent, .status, .host(id: "mac mini"), .notifications, .allThreads, .hostProjects(hostId: "mac mini"), - .projectThreads(hostId: "mac mini", projectId: "p/1"), + .projectThreads(hostId: "mac mini", projectId: "p/1"), .search(query: "bun & test?"), ] { #expect(Route(url: route.url) == route) } @@ -477,6 +576,47 @@ private let feedItemJSON = """ // Allow without a requestId cannot answer, so it opens the thread instead. #expect(NotificationCommand(actionIdentifier: "allow", userInfo: ["hostId": "h", "threadId": "t"], text: nil) == .open(.thread(hostId: "h", threadId: "t"))) + // Answer opens the thread's question screen. + #expect(NotificationCommand(actionIdentifier: "answer", userInfo: info, text: nil) == .open(.thread(hostId: "h", threadId: "t"))) + } + + @Test func questionActionsBuildAnswers() throws { + let info: [AnyHashable: Any] = [ + "hostId": "h", "threadId": "t", "requestId": "r", + "question": [ + "id": "base", "allowCustomAnswer": true, + "options": [["label": "bun:1-alpine", "value": "alpine"], ["label": "bun:1-debian", "value": "debian"]], + ], + ] + func body(_ action: String, _ text: String? = nil) throws -> [String: Any] { + let response = try #require(NotificationCommand(actionIdentifier: action, userInfo: info, text: text).response) + #expect(response.hostId == "h" && response.threadId == "t") + return try encodeObject(response.request) + } + let picked = try body("option.1") + #expect(picked["requestId"] as? String == "r") + #expect(picked["answers"] as? [String: String] == ["base": "debian"]) + #expect(try body("say", " the slim one ")["answers"] as? [String: String] == ["base": "the slim one"]) + + // An option the push does not have, an empty answer, or no question opens the thread instead. + let thread = NotificationCommand.open(.thread(hostId: "h", threadId: "t")) + #expect(NotificationCommand(actionIdentifier: "option.4", userInfo: info, text: nil) == thread) + #expect(NotificationCommand(actionIdentifier: "say", userInfo: info, text: " ") == thread) + #expect(NotificationCommand(actionIdentifier: "option.0", userInfo: ["hostId": "h", "threadId": "t", "requestId": "r"], text: nil) == thread) + } + + @MainActor @Test func questionNotificationActions() throws { + let question = PushQuestion( + id: "base", options: [.init(label: "bun:1-alpine", value: "alpine"), .init(label: "bun:1-debian", value: "debian")], + allowCustomAnswer: true) + let actions = NotificationHandler.inputActions(question) + #expect(actions.map(\.title) == ["bun:1-alpine", "bun:1-debian", "Say an answer", "Open"]) + #expect(actions.map(\.identifier) == ["option.0", "option.1", "say", "open"]) + #expect(actions[2] is UNTextInputNotificationAction) + var closed = question + closed.allowCustomAnswer = false + #expect(NotificationHandler.inputActions(closed).map(\.title) == ["bun:1-alpine", "bun:1-debian", "Open"]) + #expect(NotificationHandler.inputActions(nil).map(\.title) == ["Answer", "Open"]) } } @@ -652,6 +792,29 @@ private let feedItemJSON = """ } } + @Test func searchSkipsOfflineHosts() async throws { + let gateway = DemoGateway() + let response = try await gateway.search("healthcheck") + #expect(response.hits.map(\.thread.threadId).sorted() == ["t-container", "t-health"]) + #expect(response.hits.first { $0.thread.threadId == "t-container" }?.snippet?.contains("healthcheck") == true) + #expect(response.hits.first { $0.thread.threadId == "t-health" }?.snippet == nil) + #expect(response.unsearched == ["LittleMan"]) + // Bump Next.js is on LittleMan, which is offline. + #expect(try await gateway.search("Next.js").hits.isEmpty) + } + + @Test func approvalForTheSmartStack() async throws { + let gateway = DemoGateway() + // The demo's longest wait is a question, which gets no buttons. + #expect(await WaitingEntry.Approval.load(for: try await gateway.summary(), api: gateway) == nil) + let summary = Summary( + needsYou: 1, working: 0, next: .init(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini"), + hostsOffline: 0) + let approval = try #require(await WaitingEntry.Approval.load(for: summary, api: gateway)) + #expect(approval.requestId == "r-ocr") + #expect(approval.allow.decision == .accept && approval.deny.decision == .decline) + } + @Test func multiSelectAnswerMovesThreadToWorking() async throws { let gateway = DemoGateway() let item = try #require(try await gateway.feed().needsYou.first { $0.request?.questions.first?.multiSelect == true }) diff --git a/watch/WristCodeTests/SnapshotTests.swift b/watch/WristCodeTests/SnapshotTests.swift index 0038f06..f438680 100644 --- a/watch/WristCodeTests/SnapshotTests.swift +++ b/watch/WristCodeTests/SnapshotTests.swift @@ -27,9 +27,62 @@ struct SnapshotTests { hostId: target.hostId, threadId: target.threadId, request: .decision(requestId: item.request!.id, decision: .decline)) } - let idle = WaitingEntry(date: .now, content: .counts(try await gateway.summary(), stale: false)) + let working = WaitingEntry(date: .now, content: .counts(try await gateway.summary(), stale: false)) - try write(ComplicationGallery(waiting: waiting, waitingCount: feed.needsYou.count, idle: idle), "complication.png") + try write(ComplicationGallery(sections: [("\(feed.needsYou.count) waiting", waiting), ("Nothing waiting", working)]), "complication.png") + try write(ComplicationGallery(sections: [("\(feed.needsYou.count) waiting", waiting)]), "complication-waiting.png") + try write(ComplicationGallery(sections: [("Nothing waiting", working)]), "complication-working.png") + } + + @Test func smartStack() async throws { + // Answering the release question leaves the homelab card's subagent approval waiting longest. + let gateway = DemoGateway() + let release = try await gateway.thread(hostId: "mac-mini", threadId: "t-release") + try await gateway.respond( + hostId: "mac-mini", threadId: "t-release", + request: .answers(requestId: release.item.request!.id, answers: ["checks": .values(["unit"])])) + let summary = try await gateway.summary() + let approval = try #require(await WaitingEntry.Approval.load(for: summary, api: gateway)) + let waiting = WaitingEntry(date: .now, content: .counts(summary, stale: false), approval: approval) + + // Denying everything on a fresh gateway leaves the scaffold thread working most recently. + let idle = DemoGateway() + for item in try await idle.feed().needsYou { + let target = item.requestThread + try await idle.respond( + hostId: target.hostId, threadId: target.threadId, + request: .decision(requestId: item.request!.id, decision: .decline)) + } + let working = WaitingEntry(date: .now, content: .counts(try await idle.summary(), stale: false)) + try write(SmartStackGallery(entries: [waiting, working]), "smart-stack.png") + } + + @Test func changes() async throws { + let gateway = DemoGateway() + let feed = try await gateway.feed() + try write(feedColumn { + RecentHeader(recent: feed.recent, more: feed.moreRecent) + ForEach(feed.recent.prefix(4)) { StatusCard(item: $0) } + }, "feed-changes.png") + let detail = try await gateway.thread(hostId: "littleman", threadId: "t-next") + try write(titled("Thread", ThreadView(hostId: "littleman", threadId: "t-next", detail: detail, scrolls: false)), "thread-changes.png") + } + + @Test func search() async throws { + let gateway = DemoGateway() + let hits = try await gateway.search("healthcheck") + try write(titled("\u{201C}healthcheck\u{201D}", SearchView(query: "healthcheck", response: hits, scrolls: false)), "search.png") + let none = try await gateway.search("terraform") + try write(titled("\u{201C}terraform\u{201D}", SearchView(query: "terraform", response: none, scrolls: false)), "search-empty.png") + } + + @Test func questionNotification() throws { + let question = PushQuestion( + id: "base", options: [.init(label: "bun:1-alpine", value: "alpine"), .init(label: "bun:1-debian", value: "debian")], + allowCustomAnswer: true) + try write(NotificationPreview( + view: InputNotificationView(title: "Gateway Containerfile", question: "Which base image?"), + actions: NotificationHandler.inputActions(question).map(\.title)), "notification-question.png") } @Test func multiSelectQuestion() async throws { @@ -169,16 +222,15 @@ struct SnapshotTests { } } -/// Every family at its 46mm size, once with threads waiting and once with nothing waiting. +/// Every family at its 46mm size, once per titled entry. private struct ComplicationGallery: View { - var waiting: WaitingEntry - var waitingCount: Int - var idle: WaitingEntry + var sections: [(title: String, entry: WaitingEntry)] var body: some View { VStack(alignment: .leading, spacing: 10) { - section("\(waitingCount) waiting", waiting) - section("Nothing waiting", idle) + ForEach(sections.indices, id: \.self) { index in + section(sections[index].title, sections[index].entry) + } } .padding(.horizontal, 10) .padding(.top, 8) @@ -193,7 +245,7 @@ private struct ComplicationGallery: View { WaitingView(entry: entry, familyOverride: .accessoryInline) .font(.system(size: 13, weight: .semibold, design: .rounded)) .frame(height: 18) - WaitingView(entry: entry, familyOverride: .accessoryRectangular) + WaitingView(entry: entry, familyOverride: .accessoryRectangular, inStackOverride: false) .frame(width: 184, height: 72) HStack(spacing: 16) { // Outside a face AccessoryWidgetBackground fills a square; a face clips it round. @@ -206,3 +258,51 @@ private struct ComplicationGallery: View { } } } + +/// The rectangular widget as the Smart Stack shows it, on its card and with Allow and Deny when the +/// waiting thread wants an approval. +private struct SmartStackGallery: View { + var entries: [WaitingEntry] + + var body: some View { + VStack(spacing: 10) { + ForEach(entries.indices, id: \.self) { index in + WaitingView(entry: entries[index], familyOverride: .accessoryRectangular, inStackOverride: true) + .padding(.horizontal, 10) + .padding(.vertical, 8) + .frame(width: 196, height: 96, alignment: .topLeading) + .background(Color(white: 0.11), in: RoundedRectangle(cornerRadius: 18, style: .continuous)) + } + } + .padding(.top, 8) + .foregroundStyle(.white) + .fontDesign(.rounded) + } +} + +/// The question notification's long look over its actions. The system draws the actions; these +/// stand in for them with the same titles. +private struct NotificationPreview: View { + var view: InputNotificationView + var actions: [String] + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("WRISTCODE") + .font(.system(size: 11, weight: .semibold, design: .rounded)) + .foregroundStyle(Theme.secondaryText) + view + .padding(.bottom, 4) + ForEach(actions, id: \.self) { title in + Text(title) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .frame(maxWidth: .infinity) + .padding(.vertical, 9) + .background(Theme.control, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + } + .padding(.horizontal, 8) + .padding(.top, 10) + .foregroundStyle(.white) + } +} diff --git a/watch/WristCodeWidget/RespondIntent.swift b/watch/WristCodeWidget/RespondIntent.swift new file mode 100644 index 0000000..02e5159 --- /dev/null +++ b/watch/WristCodeWidget/RespondIntent.swift @@ -0,0 +1,37 @@ +// Allow and Deny on the Smart Stack widget. WidgetKit runs the intent without opening the app, then +// reloads the widget's timeline. A request someone already answered (409) counts as done. +// +// The app target compiles this file too, because WaitingView builds the buttons. + +import AppIntents + +struct RespondIntent: AppIntent { + static let title: LocalizedStringResource = "Answer an approval" + static let isDiscoverable = false + + @Parameter(title: "Host") var hostId: String + @Parameter(title: "Thread") var threadId: String + @Parameter(title: "Request") var requestId: String + @Parameter(title: "Decision") var decision: String + + init() {} + + init(_ approval: WaitingEntry.Approval, decision: ApprovalDecision) { + hostId = approval.hostId + threadId = approval.threadId + requestId = approval.requestId + self.decision = decision.rawValue + } + + func perform() async throws -> some IntentResult { + guard let api = SharedState.api else { return .result() } + do { + try await api.respond( + hostId: hostId, threadId: threadId, + request: .decision(requestId: requestId, decision: ApprovalDecision(rawValue: decision))) + } catch let error as GatewayError where error.isStale { + // Answered already, on the watch or elsewhere. The reload shows what is next. + } + return .result() + } +} diff --git a/watch/WristCodeWidget/WaitingView.swift b/watch/WristCodeWidget/WaitingView.swift index 20e6a65..609de4c 100644 --- a/watch/WristCodeWidget/WaitingView.swift +++ b/watch/WristCodeWidget/WaitingView.swift @@ -1,6 +1,12 @@ // One view per complication family, in mock B's colors: amber when something needs you, teal for // work in progress, and nothing that animates. Tinted faces use the accented parts only. // +// The rectangular slot switches, as mock C does: while anything waits on you it shows the counts, +// the thread that has waited longest and what it wants; otherwise the working thread changed most +// recently, how long its step has run and what it is doing. In the Smart Stack, where the widget +// has its container background, an approval also gets Allow and Deny, which answer through +// `RespondIntent` without opening the app. +// // The app target compiles this file too, so the snapshot tests can render the complication. import SwiftUI @@ -16,8 +22,20 @@ struct WaitingEntry: TimelineEntry { case unavailable } + /// The Allow and Deny pair for the summary's `next` thread, when it waits on an approval that + /// offers both. The thread named is the one that answers it, a subagent when one asked. + struct Approval: Equatable, Sendable { + var hostId: String + var threadId: String + var requestId: String + var allow: PendingRequest.Option + var deny: PendingRequest.Option + } + var date: Date var content: Content + /// Only looked up for the rectangular family, which the Smart Stack uses. + var approval: Approval? = nil /// Ranks the complication higher in the Smart Stack while something waits. var relevance: TimelineEntryRelevance? { @@ -27,21 +45,59 @@ struct WaitingEntry: TimelineEntry { static let sample = WaitingEntry(date: .now, content: .counts(Summary( needsYou: 2, working: 3, - next: .init(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini"), + next: .init(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini", request: "bun test --filter ocr"), hostsOffline: 0), stale: false)) } +extension WaitingEntry.Approval { + /// Reads `next`'s pending request. Nil unless it is an approval with Allow and Deny, or when the + /// thread cannot be read, in which case the widget opens the thread instead. + static func load(for summary: Summary, api: any GatewayAPI) async -> Self? { + guard summary.needsYou > 0, let next = summary.next, + let detail = try? await api.thread(hostId: next.hostId, threadId: next.threadId), + let request = detail.item.request, !request.isQuestion, let quick = request.quickOptions + else { return nil } + let target = detail.item.requestThread + return Self(hostId: target.hostId, threadId: target.threadId, requestId: request.id, allow: quick.allow, deny: quick.deny) + } +} + +/// What the rectangular slot shows. +enum ComplicationMode: Equatable { + /// Something waits on you: the counts, then the thread that has waited longest and what it wants. + case waiting(Summary.Next) + /// Nothing waits: the working thread changed most recently and what it is doing. + case working(Summary.Current) + /// Nothing waits on you and nothing works, or an older gateway sent no thread to name. + case idle + + init(_ summary: Summary) { + if summary.needsYou > 0, let next = summary.next { + self = .waiting(next) + } else if let current = summary.current { + self = .working(current) + } else { + self = .idle + } + } +} + struct WaitingView: View { @Environment(\.widgetFamily) private var widgetFamily + @Environment(\.showsWidgetContainerBackground) private var showsBackground var entry: WaitingEntry /// Overrides the environment's family, for rendering outside a widget. var familyOverride: WidgetFamily? + /// Overrides whether this is the Smart Stack, for rendering outside a widget. + var inStackOverride: Bool? private var family: WidgetFamily { familyOverride ?? widgetFamily } + /// Watch faces drop the container background, and the Smart Stack keeps it. + private var inStack: Bool { inStackOverride ?? showsBackground } var body: some View { content - .widgetURL(next?.route.url) + .widgetURL(route?.url) .containerBackground(for: .widget) { if waiting > 0 { LinearGradient(colors: [Theme.you.opacity(0.3), Theme.card], startPoint: .top, endPoint: .bottom) @@ -67,14 +123,15 @@ struct WaitingView: View { private var circular: some View { ZStack { AccessoryWidgetBackground() - if let summary, summary.needsYou > 0 { + if let summary, summary.needsYou > 0 || summary.working > 0 { + let waits = summary.needsYou > 0 VStack(spacing: -3) { - Text("\(summary.needsYou)") + Text("\(waits ? summary.needsYou : summary.working)") .font(.system(size: 26, weight: .bold, design: .rounded)) - .foregroundStyle(Theme.you) + .foregroundStyle(waits ? Theme.you : Theme.work) .minimumScaleFactor(0.6) .widgetAccentable() - Text("waiting") + Text(waits ? "waiting" : "working") .font(.system(size: 9, weight: .semibold, design: .rounded)) .foregroundStyle(.secondary) } @@ -95,42 +152,10 @@ struct WaitingView: View { private var rectangular: some View { VStack(alignment: .leading, spacing: 0) { if let summary { - if let next = summary.next, summary.needsYou > 0 { - HStack(alignment: .firstTextBaseline, spacing: 4) { - Text("\(summary.needsYou)") - .font(.system(size: 22, weight: .bold, design: .rounded)) - Text("waiting") - .font(.system(size: 14, weight: .semibold, design: .rounded)) - Spacer(minLength: 0) - staleMark - } - .foregroundStyle(Theme.you) - .widgetAccentable() - Text(next.title) - .font(.system(size: 15, weight: .semibold, design: .rounded)) - .lineLimit(1) - Text(next.hostLabel) - .font(.system(size: 13, design: .rounded)) - .foregroundStyle(.secondary) - .lineLimit(1) - } else { - HStack(spacing: 4) { - Image(systemName: "checkmark") - Text("Nothing waiting") - Spacer(minLength: 0) - staleMark - } - .font(.system(size: 14, weight: .semibold, design: .rounded)) - .foregroundStyle(.secondary) - Text("\(summary.working) working") - .font(.system(size: 17, weight: .bold, design: .rounded)) - .foregroundStyle(summary.working > 0 ? Theme.work : .secondary) - .widgetAccentable() - if summary.hostsOffline > 0 { - Text(summary.hostsOffline == 1 ? "1 host offline" : "\(summary.hostsOffline) hosts offline") - .font(.system(size: 13, design: .rounded)) - .foregroundStyle(.secondary) - } + switch ComplicationMode(summary) { + case let .waiting(next): waitingRows(summary, next) + case let .working(current): workingRows(summary, current) + case .idle: idleRows(summary) } } else { Text("WristCode") @@ -143,22 +168,148 @@ struct WaitingView: View { } } .frame(maxWidth: .infinity, alignment: .leading) - .accessibilityElement(children: .combine) + .accessibilityElement(children: .contain) } + /// The counts, the thread that has waited longest, and the command it wants to run or its + /// question. In the Smart Stack an approval adds Allow and Deny. + @ViewBuilder + private func waitingRows(_ summary: Summary, _ next: Summary.Next) -> some View { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text("● \(summary.needsYou) waiting") + .foregroundStyle(Theme.you) + .widgetAccentable() + Spacer(minLength: 0) + if summary.working > 0 { + Text("\(summary.working) working") + .foregroundStyle(Theme.work) + } + staleMark + } + .font(.system(size: 12, weight: .semibold, design: .rounded)) + .lineLimit(1) + Text(next.title) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .lineLimit(1) + .padding(.top, 1) + if let request = next.request { + Text(code: request) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + .lineLimit(inStack && entry.approval == nil ? 2 : 1) + } else { + Text(next.hostLabel) + .font(.system(size: 13, design: .rounded)) + .foregroundStyle(.secondary) + .lineLimit(1) + } + if inStack, let approval = entry.approval { + HStack(spacing: 5) { + Button(intent: RespondIntent(approval, decision: approval.allow.decision)) { + stackButton(approval.allow.label, fill: Theme.you, foreground: .black) + } + Button(intent: RespondIntent(approval, decision: approval.deny.decision)) { + stackButton(approval.deny.label, fill: Theme.control, foreground: .white) + } + } + .buttonStyle(.plain) + .padding(.top, 4) + } + } + + /// The working thread changed most recently with how long its step has run, the step itself, and + /// the waiting count under it. The elapsed time is as of the entry, since nothing here ticks. + @ViewBuilder + private func workingRows(_ summary: Summary, _ current: Summary.Current) -> some View { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(current.title) + .lineLimit(1) + if let startedAt = current.activity?.startedAt { + Text("· \(Format.elapsed(since: startedAt, now: entry.date))") + .monospacedDigit() + .fixedSize() + } + Spacer(minLength: 0) + staleMark + } + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(Theme.work) + .widgetAccentable() + if let activity = current.activity { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Image(systemName: activity.kind.symbol) + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(Theme.work) + Text(code: activity.text) + .font(.system(size: 12)) + .lineLimit(inStack ? 2 : 1) + } + .padding(.top, 2) + } else { + Text(summary.working == 1 ? "1 working" : "\(summary.working) working") + .font(.system(size: 13, design: .rounded)) + .foregroundStyle(.secondary) + .padding(.top, 2) + } + Text(summary.needsYou > 0 ? "● \(summary.needsYou) waiting" : "Nothing waiting") + .font(.system(size: 12, weight: .semibold, design: .rounded)) + .foregroundStyle(summary.needsYou > 0 ? Theme.you : .secondary) + .padding(.top, 3) + } + + private func idleRows(_ summary: Summary) -> some View { + Group { + HStack(spacing: 4) { + if summary.needsYou > 0 { + Text("● \(summary.needsYou) waiting") + .foregroundStyle(Theme.you) + } else { + Image(systemName: "checkmark") + Text("Nothing waiting") + } + Spacer(minLength: 0) + staleMark + } + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundStyle(.secondary) + Text("\(summary.working) working") + .font(.system(size: 17, weight: .bold, design: .rounded)) + .foregroundStyle(summary.working > 0 ? Theme.work : .secondary) + .widgetAccentable() + if summary.hostsOffline > 0 { + Text(summary.hostsOffline == 1 ? "1 host offline" : "\(summary.hostsOffline) hosts offline") + .font(.system(size: 13, design: .rounded)) + .foregroundStyle(.secondary) + } + } + } + + /// The count with the thread's title, then the counts alone, then the waiting count. private var inline: some View { ViewThatFits { + Text(titled ?? spoken) Text(spoken) if let summary { Text("\(summary.needsYou) waiting") } } } + /// "2 waiting · Receipt OCR" or "3 working · Phase 3 screens", for the inline slot. + private var titled: String? { + guard let summary else { return nil } + return switch ComplicationMode(summary) { + case let .waiting(next): "\(summary.needsYou) waiting · \(next.title)" + case let .working(current): "\(summary.working) working · \(current.title)" + case .idle: nil + } + } + private var corner: some View { Group { if let summary { - Text("\(summary.needsYou)") + let waits = summary.needsYou > 0 + Text("\(waits ? summary.needsYou : summary.working)") .font(.system(size: 24, weight: .bold, design: .rounded)) - .foregroundStyle(summary.needsYou > 0 ? Theme.you : Color.secondary) + .foregroundStyle(waits ? Theme.you : summary.working > 0 ? Theme.work : Color.secondary) .widgetAccentable() } else { Image(systemName: unpairedSymbol) @@ -168,7 +319,7 @@ struct WaitingView: View { } .widgetLabel { if let summary { - Text(summary.needsYou > 0 ? "waiting" : "\(summary.working) working") + Text(summary.needsYou > 0 ? "waiting" : "working") } else { Text("WristCode") } @@ -185,12 +336,31 @@ struct WaitingView: View { private var waiting: Int { summary?.needsYou ?? 0 } - private var next: Summary.Next? { waiting > 0 ? summary?.next : nil } + /// Where a tap goes: the thread waiting longest, else the working thread, else the feed. + private var route: Route? { + guard let summary else { return nil } + return switch ComplicationMode(summary) { + case let .waiting(next): next.route + case let .working(current): current.route + case .idle: nil + } + } private var unpairedSymbol: String { entry.content == .notPaired ? "applewatch.slash" : "wifi.slash" } + /// A Smart Stack button's label, drawn here because widgets show a button's label as is. + private func stackButton(_ title: String, fill: Color, foreground: Color) -> some View { + Text(title) + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(foreground) + .lineLimit(1) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + .background(fill, in: Capsule()) + } + /// Shown beside the count when it is the last one seen, not a fresh one. @ViewBuilder private var staleMark: some View { diff --git a/watch/WristCodeWidget/WaitingWidget.swift b/watch/WristCodeWidget/WaitingWidget.swift index 331a02c..68c9422 100644 --- a/watch/WristCodeWidget/WaitingWidget.swift +++ b/watch/WristCodeWidget/WaitingWidget.swift @@ -1,6 +1,7 @@ -// The watch face complication: how many threads are waiting on you, from `GET /v1/summary`. It -// reads the device token and the demo flag from the shared Keychain group, and the app reloads it -// whenever its own feed or a notification changes the counts. +// The watch face complication and Smart Stack widget: what waits on you or what is working, from +// `GET /v1/summary`. For the rectangular family it also reads the waiting thread, so the Smart Stack +// can offer Allow and Deny. It reads the device token and the demo flag from the shared Keychain +// group, and the app reloads it whenever its own feed or a notification changes the counts. import SwiftUI import WidgetKit @@ -18,7 +19,7 @@ struct WaitingWidget: Widget { WaitingView(entry: entry) } .configurationDisplayName("Waiting on you") - .description("Threads waiting for your approval or answer.") + .description("Threads waiting for your approval or answer, or what is working now.") .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner]) } } @@ -36,29 +37,26 @@ struct WaitingProvider: TimelineProvider { completion(.sample) return } - Task { completion(await Self.load()) } + let family = context.family + Task { completion(await Self.load(family)) } } func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> Void) { + let family = context.family Task { - let entry = await Self.load() + let entry = await Self.load(family) completion(Timeline(entries: [entry], policy: .after(entry.date.addingTimeInterval(Self.refreshInterval)))) } } - static func load() async -> WaitingEntry { - let api: any GatewayAPI - if SharedState.demo { - api = DemoGateway.shared - } else if let credentials = DeviceStore.load() { - api = GatewayClient(baseURL: credentials.gatewayURL, token: credentials.token) - } else { - return WaitingEntry(date: .now, content: .notPaired) - } + /// The summary, and for the rectangular family the waiting approval's Allow and Deny. + static func load(_ family: WidgetFamily) async -> WaitingEntry { + guard let api = SharedState.api else { return WaitingEntry(date: .now, content: .notPaired) } do { let summary = try await api.summary() SharedState.lastSummary = summary - return WaitingEntry(date: .now, content: .counts(summary, stale: false)) + let approval = family == .accessoryRectangular ? await WaitingEntry.Approval.load(for: summary, api: api) : nil + return WaitingEntry(date: .now, content: .counts(summary, stale: false), approval: approval) } catch GatewayError.unauthorized { return WaitingEntry(date: .now, content: .notPaired) } catch { @@ -69,9 +67,12 @@ struct WaitingProvider: TimelineProvider { } private func entry(_ needsYou: Int, _ working: Int, next: Bool = true) -> WaitingEntry { - let thread = Summary.Next(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini") + let thread = Summary.Next(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini", request: "bun test --filter ocr") + let current = Summary.Current( + hostId: "mac-mini", threadId: "t-scaffold", title: "Watch app scaffold", + activity: Activity(kind: .command, text: "bun test", startedAt: .now.addingTimeInterval(-42))) return WaitingEntry(date: .now, content: .counts( - Summary(needsYou: needsYou, working: working, next: needsYou > 0 && next ? thread : nil, hostsOffline: 1), + Summary(needsYou: needsYou, working: working, next: needsYou > 0 && next ? thread : nil, current: working > 0 ? current : nil, hostsOffline: 1), stale: false)) } diff --git a/watch/project.yml b/watch/project.yml index 5d5d004..f502197 100644 --- a/watch/project.yml +++ b/watch/project.yml @@ -19,8 +19,9 @@ targets: platform: watchOS sources: - WristCode - # The complication's views, so the snapshot tests can render them. + # The complication's views and the intent its buttons run, so the snapshot tests can render them. - WristCodeWidget/WaitingView.swift + - WristCodeWidget/RespondIntent.swift dependencies: # Embedded in the watch app's PlugIns folder. - target: WristCodeWidget diff --git a/watch/screenshots/browse-hosts.png b/watch/screenshots/browse-hosts.png index 6d7ba6e627f9e3addc71e328cb57f47ac7d2c682..9318c19f285a55a537949f7c3bc57d8b97632f02 100644 GIT binary patch literal 32930 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 literal 26708 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 diff --git a/watch/screenshots/complication-waiting.png b/watch/screenshots/complication-waiting.png new file mode 100644 index 0000000000000000000000000000000000000000..69879ef7694bf2a74a6794a3ff648369b04e4107 GIT binary patch literal 29640 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 Ab^rhX literal 0 HcmV?d00001 diff --git a/watch/screenshots/complication-working.png b/watch/screenshots/complication-working.png new file mode 100644 index 0000000000000000000000000000000000000000..88f6c2fded8c818f0a4c336c6d8078906b6a8f77 GIT binary patch literal 32131 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
r(fizIPW+A@*z~A^em|Xdv>ENz1p2o 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