Watch: notification switches, a model list and Ask before acting on new threads, and Ask before acting on a thread

This commit is contained in:
gib committed 2026-11-02 12:10:48 -05:00
1 parent 3cc7e40852
commit 6bc9cac6ca
14 files changed
+623 -81

No files matched your search

+43 -9
View File
@@ -1,7 +1,7 @@
// Fixture data shaped like the docs/api.md examples, served by an in-memory gateway. Demo mode and
// 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, and so on.
// hides a thread from the feed, Ask before acting and the notification switches stick, and so on.
import Foundation
@@ -15,6 +15,8 @@ actor DemoGateway: GatewayAPI {
private var statusResponse: StatusResponse
/// Ids of settled threads, which the feed leaves out.
private var settled: Set<String> = []
/// Ids of threads in Ask before acting.
private var askingFirst: Set<String> = []
private var nextId = 1
init(now: Date = .now) {
@@ -36,7 +38,9 @@ actor DemoGateway: GatewayAPI {
func thread(hostId: String, threadId: String) async throws -> ThreadDetail {
guard let item = item(hostId, threadId) else { throw GatewayError.http(status: 404, message: "Unknown thread.") }
return ThreadDetail(item: item, messages: messages[item.id] ?? [], canStop: item.phase.isWorking)
return ThreadDetail(
item: item, messages: messages[item.id] ?? [], canStop: item.phase.isWorking,
askFirst: askingFirst.contains(item.id))
}
func sendMessage(hostId: String, threadId: String, text: String) async throws {
@@ -88,6 +92,11 @@ actor DemoGateway: GatewayAPI {
settled.insert(item.id)
}
func setAskFirst(hostId: String, threadId: String, askFirst: Bool) async throws {
guard let item = item(hostId, threadId) else { throw GatewayError.http(status: 404, message: "Unknown thread.") }
if askFirst { askingFirst.insert(item.id) } else { askingFirst.remove(item.id) }
}
func recent() async throws -> RecentResponse {
RecentResponse(items: DemoFixtures.recent(items.filter { !settled.contains($0.id) }))
}
@@ -106,6 +115,9 @@ actor DemoGateway: GatewayAPI {
guard let host = projectHosts.first(where: { $0.id == request.hostId }),
let project = host.projects.first(where: { $0.id == request.projectId })
else { throw GatewayError.http(status: 404, message: "Unknown project.") }
if let model = request.model, !host.offers(model) {
throw GatewayError.http(status: 400, message: "\(host.label) does not offer \(model.slug).")
}
let threadId = "demo-\(newId())"
let item = FeedItem(
hostId: host.id, hostLabel: host.label, threadId: threadId, projectId: project.id,
@@ -113,6 +125,7 @@ actor DemoGateway: GatewayAPI {
updatedAt: .now, lastMessage: nil, request: nil)
items.append(item)
messages[item.id] = [Message(id: newId(), role: .user, text: request.text, createdAt: .now)]
if request.askFirst == true { askingFirst.insert(item.id) }
return NewThreadResponse(hostId: host.id, threadId: threadId)
}
@@ -130,6 +143,13 @@ actor DemoGateway: GatewayAPI {
return result
}
func setNotification(_ key: NotificationPrefs.Key, _ on: Bool) async throws -> NotificationPrefs {
var prefs = statusResponse.device.notifications ?? NotificationPrefs(needsMe: true, finished: true, failed: true, subagents: true)
prefs[key] = on
statusResponse.device.notifications = prefs
return prefs
}
func unpair() async throws {}
// MARK: - Helpers
@@ -331,21 +351,33 @@ struct DemoFixtures {
],
]
let claude = AgentProvider(
instanceId: "claudeAgent", name: "Claude", canAskFirst: true,
models: [
.init(slug: "claude-opus-5-5", name: "Claude Opus 5.5"),
.init(slug: "claude-sonnet-5-5", name: "Claude Sonnet 5.5"),
.init(slug: "claude-haiku-5-5", name: "Claude Haiku 5.5"),
])
let codex = AgentProvider(
instanceId: "codex", name: "Codex", canAskFirst: true,
models: [.init(slug: "gpt-5.5", name: "GPT-5.5"), .init(slug: "gpt-5-codex", name: "GPT-5 Codex")])
projectHosts = [
.init(
id: "mac-mini", label: "Mac mini", status: .online, error: nil,
projects: [
.init(id: "p-wc-mac", title: "wristcode", model: "claude-opus-5-5"),
.init(id: "p-bill", title: "bill-tracker", model: "claude-sonnet-5-5"),
.init(id: "p-cdn", title: "cdn", model: "gpt-5-codex"),
.init(id: "p-wc-mac", title: "wristcode", model: "claude-opus-5-5", instanceId: "claudeAgent"),
.init(id: "p-bill", title: "bill-tracker", model: "claude-sonnet-5-5", instanceId: "claudeAgent"),
.init(id: "p-cdn", title: "cdn", model: "gpt-5-codex", instanceId: "codex"),
.init(id: "p-scratch", title: "scratch", model: nil),
]),
],
providers: [claude, codex]),
.init(
id: "server-gib", label: "server-gib", status: .online, error: nil,
projects: [.init(id: "p-wc", title: "wristcode", model: "claude-opus-5-5")]),
projects: [.init(id: "p-wc", title: "wristcode", model: "claude-opus-5-5", instanceId: "claudeAgent")],
providers: [claude]),
.init(
id: "littleman", label: "LittleMan", status: .offline, error: "Connection refused",
projects: [.init(id: "p-site", title: "gbrown.org", model: "claude-opus-5-5")]),
projects: [.init(id: "p-site", title: "gbrown.org", model: "claude-opus-5-5", instanceId: "claudeAgent")]),
]
// Mac mini and server-gib on their first route, Laptop on its VPN fallback, LittleMan offline.
@@ -382,7 +414,9 @@ struct DemoFixtures {
tokenExpiresAt: days(29.5), serverVersion: "0.1.4"),
],
gateway: .init(startedAt: ago(600), pushConfigured: true, lastPush: PushResult(ok: true, at: ago(2), status: 200, reason: nil)),
device: .init(name: "Apple Watch", pairedAt: days(-3), pushRegistered: true, apnsEnvironment: .production))
device: .init(
name: "Apple Watch", pairedAt: days(-3), pushRegistered: true, apnsEnvironment: .production,
notifications: NotificationPrefs(needsMe: true, finished: true, failed: true, subagents: false)))
}
/// The feed the gateway builds from these threads: needs-you oldest first, working and Recent