Watch: show subagent requests on the parent's card, and settle threads by swipe, Settle all and the thread screen
This commit is contained in:
1 parent
a4754b65b0
commit
cf8995e63f
16 files changed
+347
-31
No files matched your search
@@ -1,6 +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, and so on.
|
||||
// answering an approval moves the thread to working, a reply restarts a finished thread, settling
|
||||
// hides a thread from the feed, and so on.
|
||||
|
||||
import Foundation
|
||||
|
||||
@@ -12,6 +13,8 @@ actor DemoGateway: GatewayAPI {
|
||||
private let hosts: [HostSummary]
|
||||
private let projectHosts: [ProjectsResponse.Host]
|
||||
private var statusResponse: StatusResponse
|
||||
/// Ids of settled threads, which the feed leaves out.
|
||||
private var settled: Set<String> = []
|
||||
private var nextId = 1
|
||||
|
||||
init(now: Date = .now) {
|
||||
@@ -24,6 +27,7 @@ actor DemoGateway: GatewayAPI {
|
||||
}
|
||||
|
||||
func feed() async throws -> Feed {
|
||||
let items = items.filter { !settled.contains($0.id) }
|
||||
let needsYou = items.filter(\.phase.needsYou).sorted { $0.updatedAt < $1.updatedAt }
|
||||
let working = items.filter(\.phase.isWorking).sorted { $0.updatedAt > $1.updatedAt }
|
||||
let recent = items.filter { $0.phase == .completed || $0.phase == .failed }
|
||||
@@ -42,6 +46,7 @@ actor DemoGateway: GatewayAPI {
|
||||
|
||||
func sendMessage(hostId: String, threadId: String, text: String) async throws {
|
||||
try update(hostId, threadId) { item in
|
||||
settled.remove(item.id)
|
||||
append(Message(id: newId(), role: .user, text: text, createdAt: .now), to: item.id)
|
||||
if !item.phase.isWorking {
|
||||
item.phase = .running
|
||||
@@ -50,10 +55,18 @@ actor DemoGateway: GatewayAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers go to the thread that asked. For a card showing a subagent's request, that is the
|
||||
/// subagent in `via`, and the parent itself has nothing to answer.
|
||||
func respond(hostId: String, threadId: String, request: RespondRequest) async throws {
|
||||
try update(hostId, threadId) { item in
|
||||
guard item.request != nil else { throw GatewayError.http(status: 409, message: "Already answered.") }
|
||||
let index = items.firstIndex { $0.requestThread == (hostId, threadId) }
|
||||
?? items.firstIndex { $0.hostId == hostId && $0.threadId == threadId }
|
||||
guard let index else { throw GatewayError.http(status: 404, message: "Unknown thread.") }
|
||||
try update(items[index].hostId, items[index].threadId) { item in
|
||||
guard item.request != nil, item.requestThread == (hostId, threadId) else {
|
||||
throw GatewayError.http(status: 409, message: "Already answered.")
|
||||
}
|
||||
item.request = nil
|
||||
item.via = nil
|
||||
switch request {
|
||||
case let .decision(_, decision) where decision == .decline || decision == .cancel:
|
||||
item.phase = .completed
|
||||
@@ -75,6 +88,11 @@ actor DemoGateway: GatewayAPI {
|
||||
}
|
||||
}
|
||||
|
||||
func settle(hostId: String, threadId: String) async throws {
|
||||
guard let item = item(hostId, threadId) else { throw GatewayError.http(status: 404, message: "Unknown thread.") }
|
||||
settled.insert(item.id)
|
||||
}
|
||||
|
||||
func projects() async throws -> ProjectsResponse {
|
||||
ProjectsResponse(hosts: projectHosts)
|
||||
}
|
||||
@@ -194,6 +212,17 @@ struct DemoFixtures {
|
||||
allowCustomAnswer: true, multiSelect: true),
|
||||
]))
|
||||
|
||||
// A subagent's request, shown on the card of the thread that started it.
|
||||
let homelab = FeedItem(
|
||||
hostId: "mac-mini", hostLabel: "Mac mini", threadId: "t-homelab", projectId: "p-wristlab",
|
||||
projectTitle: "WristLab", title: "HomeLab Stats Apple Watch App", phase: .waitingForApproval,
|
||||
updatedAt: ago(4), lastMessage: "Delegated a PATH check to the Mac mini. Waiting on it before I wire up the build.",
|
||||
request: PendingRequest(
|
||||
id: "r-path", kind: "command", prompt: "brew install jq", reason: nil,
|
||||
options: [.init(decision: .accept, label: "Allow"), .init(decision: .decline, label: "Deny")],
|
||||
questions: []),
|
||||
via: .init(hostId: "mac-mini", threadId: "t-path", title: "Check Panama PATH"))
|
||||
|
||||
let scaffold = FeedItem(
|
||||
hostId: "mac-mini", hostLabel: "Mac mini", threadId: "t-scaffold", projectId: "p-wc-mac",
|
||||
projectTitle: "wristcode", title: "Watch app scaffold", phase: .running, updatedAt: ago(0.5),
|
||||
@@ -214,7 +243,18 @@ struct DemoFixtures {
|
||||
projectTitle: "cdn", title: "Fix CDN headers", phase: .failed, updatedAt: ago(140),
|
||||
lastMessage: "cargo test failed: 2 tests in headers.rs", request: nil)
|
||||
|
||||
items = [approval, question, release, scaffold, containerfile, nextjs, cdn]
|
||||
let statusScreens = FeedItem(
|
||||
hostId: "laptop", hostLabel: "Laptop", threadId: "t-status", projectId: "p-wc-laptop",
|
||||
projectTitle: "wristcode", title: "Status screens", phase: .completed, updatedAt: ago(35),
|
||||
lastMessage: "Done. Status, environment and notification screens are in TestFlight build 202611011516.",
|
||||
request: nil)
|
||||
|
||||
let hyprlock = FeedItem(
|
||||
hostId: "server-gib", hostLabel: "server-gib", threadId: "t-hyprlock", projectId: "p-panama",
|
||||
projectTitle: "Panama", title: "Fix hyprlock crash", phase: .completed, updatedAt: ago(95),
|
||||
lastMessage: "Fixed. hyprlock no longer crashes when a monitor sleeps.", request: nil)
|
||||
|
||||
items = [approval, question, release, homelab, scaffold, containerfile, statusScreens, nextjs, hyprlock, cdn]
|
||||
|
||||
func m(_ id: String, _ role: Message.Role, _ minutes: Double, _ text: String) -> Message {
|
||||
Message(id: id, role: role, text: text, createdAt: ago(minutes))
|
||||
@@ -234,6 +274,10 @@ struct DemoFixtures {
|
||||
m("r1", .user, 25, "Get the watch app ready for TestFlight."),
|
||||
m("r2", .assistant, 6, "The build is ready. Which checks should run first?"),
|
||||
],
|
||||
homelab.id: [
|
||||
m("h1", .user, 30, "Build a HomeLab stats app for the watch."),
|
||||
m("h2", .assistant, 4, "Delegated a PATH check to the Mac mini. Waiting on it before I wire up the build."),
|
||||
],
|
||||
scaffold.id: [
|
||||
m("c1", .user, 4, "Scaffold the watch app with XcodeGen."),
|
||||
m("c2", .assistant, 0.5, "Writing ThreadListView.swift"),
|
||||
@@ -247,6 +291,14 @@ struct DemoFixtures {
|
||||
m("e2", .assistant, 80, "Updating next and eslint-config-next, then running the build."),
|
||||
m("e3", .assistant, 62, "Done. Next.js is on 17.1 and the build passes. Want me to commit it?"),
|
||||
],
|
||||
statusScreens.id: [
|
||||
m("s1", .user, 120, "Build the status, environment and notification screens."),
|
||||
m("s2", .assistant, 35, "Done. Status, environment and notification screens are in TestFlight build 202611011516."),
|
||||
],
|
||||
hyprlock.id: [
|
||||
m("k1", .user, 110, "hyprlock crashes when a monitor sleeps. Fix it."),
|
||||
m("k2", .assistant, 95, "Fixed. hyprlock no longer crashes when a monitor sleeps."),
|
||||
],
|
||||
cdn.id: [
|
||||
m("f1", .user, 150, "Fix the cache headers the CDN sends for images."),
|
||||
m("f2", .assistant, 140, "cargo test failed: 2 tests in headers.rs"),
|
||||
|
||||
Reference in new issue
Block a user