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
@@ -60,6 +60,20 @@ private let feedItemJSON = """
|
||||
#expect(request.summary == "bun test --filter ocr")
|
||||
}
|
||||
|
||||
@Test func subagentRequest() throws {
|
||||
let item = try decode(FeedItem.self, feedItemJSON.replacingOccurrences(
|
||||
of: "\"request\": {",
|
||||
with: #""via": { "hostId": "mac", "threadId": "sub", "title": "Check Panama PATH" }, "request": {"#))
|
||||
#expect(item.via == FeedItem.Via(hostId: "mac", threadId: "sub", title: "Check Panama PATH"))
|
||||
#expect(item.requestThread == ("mac", "sub"))
|
||||
#expect(item.route == .thread(hostId: "...", threadId: "..."))
|
||||
|
||||
// A gateway older than `via` leaves it out, and the request belongs to the thread itself.
|
||||
let plain = try decode(FeedItem.self, feedItemJSON)
|
||||
#expect(plain.via == nil)
|
||||
#expect(plain.requestThread == ("...", "..."))
|
||||
}
|
||||
|
||||
@Test func questionRequest() throws {
|
||||
let request = try decode(PendingRequest.self, """
|
||||
{ "id": "r1", "kind": "user_input", "prompt": null, "reason": null, "options": [],
|
||||
@@ -391,7 +405,7 @@ private let feedItemJSON = """
|
||||
@Test func allowMovesThreadToWorking() async throws {
|
||||
let gateway = DemoGateway()
|
||||
let before = try await gateway.feed()
|
||||
let item = try #require(before.needsYou.first { $0.request?.quickOptions != nil })
|
||||
let item = try #require(before.needsYou.first { $0.request?.quickOptions != nil && $0.via == nil })
|
||||
try await gateway.respond(
|
||||
hostId: item.hostId, threadId: item.threadId,
|
||||
request: .decision(requestId: item.request!.id, decision: .accept))
|
||||
@@ -405,6 +419,31 @@ private let feedItemJSON = """
|
||||
}
|
||||
}
|
||||
|
||||
@Test func subagentApprovalGoesToTheSubagent() async throws {
|
||||
let gateway = DemoGateway()
|
||||
let item = try #require(try await gateway.feed().needsYou.first { $0.via != nil })
|
||||
let via = try #require(item.via)
|
||||
let decision = RespondRequest.decision(requestId: item.request!.id, decision: .accept)
|
||||
// The parent has no request of its own to answer.
|
||||
await #expect(throws: GatewayError.http(status: 409, message: "Already answered.")) {
|
||||
try await gateway.respond(hostId: item.hostId, threadId: item.threadId, request: decision)
|
||||
}
|
||||
try await gateway.respond(hostId: via.hostId, threadId: via.threadId, request: decision)
|
||||
let after = try await gateway.feed()
|
||||
#expect(!after.needsYou.contains { $0.id == item.id })
|
||||
#expect(after.working.first { $0.id == item.id }?.via == nil)
|
||||
}
|
||||
|
||||
@Test func settleLeavesTheFeed() async throws {
|
||||
let gateway = DemoGateway()
|
||||
let item = try #require(try await gateway.feed().recent.first)
|
||||
try await gateway.settle(hostId: item.hostId, threadId: item.threadId)
|
||||
#expect(!(try await gateway.feed().recent.contains { $0.id == item.id }))
|
||||
// A reply brings it back.
|
||||
try await gateway.sendMessage(hostId: item.hostId, threadId: item.threadId, text: "One more thing")
|
||||
#expect(try await gateway.feed().working.contains { $0.id == item.id })
|
||||
}
|
||||
|
||||
@Test func multiSelectAnswerMovesThreadToWorking() async throws {
|
||||
let gateway = DemoGateway()
|
||||
let item = try #require(try await gateway.feed().needsYou.first { $0.request?.questions.first?.multiSelect == true })
|
||||
|
||||
@@ -22,13 +22,14 @@ struct SnapshotTests {
|
||||
let waiting = WaitingEntry(date: .now, content: .counts(try await gateway.summary(), stale: false))
|
||||
let feed = try await gateway.feed()
|
||||
for item in feed.needsYou {
|
||||
let target = item.requestThread
|
||||
try await gateway.respond(
|
||||
hostId: item.hostId, threadId: item.threadId,
|
||||
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))
|
||||
|
||||
try write(ComplicationGallery(waiting: waiting, idle: idle), "complication.png")
|
||||
try write(ComplicationGallery(waiting: waiting, waitingCount: feed.needsYou.count, idle: idle), "complication.png")
|
||||
}
|
||||
|
||||
@Test func multiSelectQuestion() async throws {
|
||||
@@ -42,6 +43,30 @@ struct SnapshotTests {
|
||||
try write(view, "thread-multiselect.png")
|
||||
}
|
||||
|
||||
@Test func subagentCard() async throws {
|
||||
let feed = try await DemoGateway().feed()
|
||||
let item = try #require(feed.needsYou.first { $0.via != nil })
|
||||
try write(feedColumn { NeedsYouCard(item: item) }, "feed-subagent.png")
|
||||
}
|
||||
|
||||
@Test func recentWithSettleAll() async throws {
|
||||
let feed = try await DemoGateway().feed()
|
||||
try write(feedColumn {
|
||||
RecentHeader(recent: feed.recent)
|
||||
ForEach(feed.recent) { StatusCard(item: $0) }
|
||||
}, "feed-recent.png")
|
||||
}
|
||||
|
||||
@Test func subagentThread() async throws {
|
||||
let detail = try await DemoGateway().thread(hostId: "mac-mini", threadId: "t-homelab")
|
||||
try write(titled("Thread", ThreadView(hostId: "mac-mini", threadId: "t-homelab", detail: detail, scrolls: false)), "thread-subagent.png")
|
||||
}
|
||||
|
||||
@Test func finishedThreadWithSettle() async throws {
|
||||
let detail = try await DemoGateway().thread(hostId: "laptop", threadId: "t-status")
|
||||
try write(titled("Thread", ThreadView(hostId: "laptop", threadId: "t-status", detail: detail, scrolls: false)), "thread-settle.png")
|
||||
}
|
||||
|
||||
@Test func statusScreens() async throws {
|
||||
let status = try await DemoGateway().status()
|
||||
try write(titled("Status", StatusView(status: status, scrolls: false)), "status.png")
|
||||
@@ -50,6 +75,18 @@ struct SnapshotTests {
|
||||
try write(titled("Notifications", NotificationsView(status: status, result: result, scrolls: false)), "status-notifications.png")
|
||||
}
|
||||
|
||||
/// Feed rows stacked as the feed's List lays them out, which ImageRenderer leaves blank.
|
||||
private func feedColumn(@ViewBuilder _ rows: () -> some View) -> some View {
|
||||
VStack(spacing: 6) {
|
||||
rows()
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.top, 10)
|
||||
.environment(AppModel.preview())
|
||||
.fontDesign(.rounded)
|
||||
.tint(Theme.work)
|
||||
}
|
||||
|
||||
/// The screen under a title like the navigation bar's, which ImageRenderer leaves out.
|
||||
private func titled(_ title: String, _ view: some View) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
@@ -85,11 +122,12 @@ struct SnapshotTests {
|
||||
/// Every family at its 46mm size, once with threads waiting and once with nothing waiting.
|
||||
private struct ComplicationGallery: View {
|
||||
var waiting: WaitingEntry
|
||||
var waitingCount: Int
|
||||
var idle: WaitingEntry
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
section("3 waiting", waiting)
|
||||
section("\(waitingCount) waiting", waiting)
|
||||
section("Nothing waiting", idle)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
Reference in new issue
Block a user