Watch: the Subagents section on the thread screen collapses behind a header with the count and how many wait or work, and opens on its own when one waits

This commit is contained in:
gib committed 2026-11-02 14:12:45 -05:00
1 parent 480db90e98
commit d10df56cc9
6 files changed
+51 -13

No files matched your search

+2 -2
View File
@@ -60,8 +60,8 @@ xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen thread
status screens, the subagent card and thread, Recent with Settle all and the "N more" row, the full
Recent list, a finished thread with Settle, a running thread with Ask before acting, New thread
with its model list, the working cards' activity lines, All threads' hosts and a project's threads,
a thread's subagents, Load earlier with markdown, the changes line and Changed list, search and
its empty state, the complication's two modes, the Smart Stack widget, and a question
a thread's subagents collapsed and open, Load earlier with markdown, the changes line and Changed
list, search and its empty state, the complication's two modes, the Smart Stack widget, and a question
notification are rendered by `SnapshotTests` instead, which only run when given a directory:
```sh
+48 -11
View File
@@ -1,6 +1,6 @@
// One thread: the agent's latest message large, older messages one scroll up with Load earlier above
// them, the pending approval or question with its options, the thread's subagents, the files its
// latest run changed with their line counts, Reply by
// them, the pending approval or question with its options, the thread's subagents under a header
// that collapses them, the files its latest run changed with their line counts, Reply by
// dictation or Scribble, Settle once it finished or failed, an Ask before acting switch, and Stop
// while a run is active. A subagent's request shows first, labeled with the subagent. Message text
// renders as markdown.
@@ -29,21 +29,26 @@ struct ThreadView: View {
/// Pages Load earlier fetched. The 10 s refresh replaces only the latest window.
@State private var history = MessageHistory()
@State private var loadingEarlier = false
/// Whether the Subagents section is open, once tapped. Nil follows the default: open while a
/// subagent waits on you, closed otherwise. Kept for as long as the screen is.
@State private var subagentsOpen: Bool?
/// Snapshots render the column without its ScrollView, which ImageRenderer leaves blank.
private let scrolls: Bool
/// `detail` and `selections` start the screen with a thread loaded and options ticked, for
/// snapshots and previews.
/// `detail`, `selections` and `subagentsOpen` start the screen with a thread loaded, options
/// ticked and the Subagents section set, for snapshots and previews.
init(
hostId: String, threadId: String,
detail: ThreadDetail? = nil, selections: [String: Set<String>] = [:], scrolls: Bool = true
detail: ThreadDetail? = nil, selections: [String: Set<String>] = [:], subagentsOpen: Bool? = nil,
scrolls: Bool = true
) {
self.hostId = hostId
self.threadId = threadId
self.scrolls = scrolls
_detail = State(initialValue: detail)
_selections = State(initialValue: selections)
_subagentsOpen = State(initialValue: subagentsOpen)
}
var body: some View {
@@ -144,11 +149,14 @@ struct ThreadView: View {
}
if !detail.subagents.isEmpty {
sectionLabel("Subagents")
ForEach(detail.subagents) { row in
ThreadLine(row: row) {
Text(row.statusWord)
.foregroundStyle(row.isActive ? row.color : Theme.secondaryText)
let open = subagentsOpen ?? detail.subagents.contains { $0.statusWord == "waiting" }
subagentsHeader(detail.subagents, open: open)
if open {
ForEach(detail.subagents) { row in
ThreadLine(row: row) {
Text(row.statusWord)
.foregroundStyle(row.isActive ? row.color : Theme.secondaryText)
}
}
}
}
@@ -200,7 +208,7 @@ struct ThreadView: View {
}
}
/// A small grey title over a list, such as Subagents or Changed.
/// A small grey title over a list, such as Changed.
private func sectionLabel(_ title: String) -> some View {
Text(title)
.font(.system(size: 11, weight: .semibold))
@@ -209,6 +217,35 @@ struct ThreadView: View {
.padding(.top, 4)
}
/// The Subagents title as a row that opens and closes the list, with the count, how many wait or
/// work, and a chevron on the right, as in "2 · 1 working".
private func subagentsHeader(_ rows: [ThreadRow], open: Bool) -> some View {
let counts = ["waiting", "working"].compactMap { word -> String? in
let count = rows.count { $0.statusWord == word }
return count > 0 ? "\(count) \(word)" : nil
}
let summary = (["\(rows.count)"] + counts).joined(separator: " · ")
return Button { subagentsOpen = !open } label: {
HStack(spacing: 4) {
Text("Subagents")
Spacer(minLength: 4)
Text(summary)
.lineLimit(1)
Image(systemName: open ? "chevron.down" : "chevron.right")
.font(.system(size: 10, weight: .semibold))
.frame(width: 12)
}
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(Theme.secondaryText)
.padding(.horizontal, 6)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("Subagents, \(summary)")
.accessibilityValue(open ? "Expanded" : "Collapsed")
}
@ViewBuilder
private func requestCards(_ request: PendingRequest, from via: FeedItem.Via?) -> some View {
if request.isQuestion {
+1
View File
@@ -127,6 +127,7 @@ struct SnapshotTests {
@Test func threadWithSubagents() async throws {
let detail = try await DemoGateway().thread(hostId: "server-gib", threadId: "t-container")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-container", detail: detail, scrolls: false)), "thread-subagents.png")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-container", detail: detail, subagentsOpen: true, scrolls: false)), "thread-subagents-open.png")
}
@Test func threadWithEarlierAndMarkdown() async throws {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 65 KiB