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:
1 parent
3cc7e40852
commit
6bc9cac6ca
14 files changed
+623
-81
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user