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:
gib committed 2026-11-01 20:26:38 -05:00
1 parent a4754b65b0
commit cf8995e63f
16 files changed
+347 -31

No files matched your search

+60 -7
View File
@@ -1,5 +1,6 @@
// Mock B's attention feed: the count waiting on you, then cards for every host. Needs you first
// (with Allow and Deny on the card), then working (with what the agent is doing), then recent.
// A Recent row swipes left to settle, and the Recent header settles every completed thread.
import SwiftUI
@@ -14,16 +15,27 @@ struct FeedView: View {
.feedRow()
if let feed = model.feed {
let recent = feed.recent.filter { !model.settling.contains($0.id) }
ForEach(feed.needsYou) { item in
NeedsYouCard(item: item).feedRow()
}
ForEach(feed.working) { item in
StatusCard(item: item).feedRow()
}
ForEach(feed.recent) { item in
StatusCard(item: item).feedRow()
if !recent.isEmpty {
RecentHeader(recent: recent).feedRow()
}
if feed.needsYou.isEmpty && feed.working.isEmpty && feed.recent.isEmpty {
ForEach(recent) { item in
StatusCard(item: item)
.feedRow()
.swipeActions(edge: .trailing) {
Button { Task { await model.settle([item]) } } label: {
Label("Settle", systemImage: "checkmark")
}
.tint(Theme.work)
}
}
if feed.needsYou.isEmpty && feed.working.isEmpty && recent.isEmpty {
Text("No threads in the last day.")
.font(.system(size: 13))
.foregroundStyle(Theme.secondaryText)
@@ -118,8 +130,33 @@ private struct FeedHeader: View {
}
}
/// "Recent" over the finished and failed threads, with Settle all when any completed. Settle all
/// leaves failed threads, which settle one at a time by swipe.
struct RecentHeader: View {
@Environment(AppModel.self) private var model
var recent: [FeedItem]
var body: some View {
let completed = recent.filter { $0.phase == .completed }
HStack {
Text("Recent")
.foregroundStyle(Theme.secondaryText)
Spacer()
if !completed.isEmpty {
Button("Settle all") { Task { await model.settle(completed) } }
.buttonStyle(.plain)
.foregroundStyle(Theme.work)
}
}
.font(.system(size: 12, weight: .semibold))
.padding(.horizontal, 8)
.padding(.top, 4)
}
}
/// An amber card for a thread waiting on an approval or an answer, with Allow and Deny inline.
private struct NeedsYouCard: View {
/// When a subagent asked, one line names it, and the answer goes to the subagent.
struct NeedsYouCard: View {
@Environment(AppModel.self) private var model
var item: FeedItem
@State private var sending = false
@@ -136,6 +173,14 @@ private struct NeedsYouCard: View {
Text(item.title)
.font(.system(size: 15, weight: .semibold))
.lineLimit(2)
if let via = item.via, let request = item.request {
let name = Text(via.title).bold().foregroundStyle(.white)
Text(request.isQuestion ? "Subagent \(name) asks:" : "Subagent \(name) wants to run:")
.font(.system(size: 12))
.foregroundStyle(Theme.secondaryText)
.lineLimit(2)
.padding(.top, 2)
}
if let request = item.request {
Text(request.summary)
.font(request.isQuestion ? .system(size: 12) : .system(size: 12, design: .monospaced))
@@ -179,8 +224,9 @@ private struct NeedsYouCard: View {
error = nil
Task {
do {
let target = item.requestThread
try await model.api.respond(
hostId: item.hostId, threadId: item.threadId,
hostId: target.hostId, threadId: target.threadId,
request: .decision(requestId: request.id, decision: decision))
} catch let failure as GatewayError where failure.isStale {
// Someone answered it already; the refresh below drops the card.
@@ -193,8 +239,9 @@ private struct NeedsYouCard: View {
}
}
/// A teal card for a working thread, or a grey one for a recent thread.
private struct StatusCard: View {
/// A teal card for a working thread, or a grey one for a recent thread. A recent thread that failed
/// to settle shows why.
struct StatusCard: View {
@Environment(AppModel.self) private var model
var item: FeedItem
@@ -217,6 +264,12 @@ private struct StatusCard: View {
.lineLimit(2)
.padding(.top, 3)
}
if let error = model.settleErrors[item.id] {
Text(error)
.font(.system(size: 11))
.foregroundStyle(Theme.secondaryText)
.padding(.top, 4)
}
}
.contentShape(Rectangle())
}